lecodes-cli 0.16.2 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1049 -444
- package/package.json +4 -3
- package/runtime/sdk/gl/Animator.ts +10 -2
- package/runtime/sdk/gl/Node.ts +6 -1
- package/runtime/sdk/gl/Scene.ts +291 -289
- package/runtime/sdk/gl/state.ts +6 -6
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -215816,7 +215816,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
215816
215816
|
// package.json
|
|
215817
215817
|
var package_default = {
|
|
215818
215818
|
name: "lecodes-cli",
|
|
215819
|
-
version: "0.
|
|
215819
|
+
version: "0.17.0",
|
|
215820
215820
|
dependencies: {
|
|
215821
215821
|
"@letary/chisel": "^0.8.0",
|
|
215822
215822
|
jimp: "^1.6.1"
|
|
@@ -215827,11 +215827,12 @@ var package_default = {
|
|
|
215827
215827
|
"qrcode-terminal": "^0.12.0",
|
|
215828
215828
|
"lecodes-3d-editor": "workspace:*",
|
|
215829
215829
|
"lecodes-assets": "workspace:*",
|
|
215830
|
-
sdk: "workspace:*"
|
|
215830
|
+
sdk: "workspace:*",
|
|
215831
|
+
"viewer-lite": "workspace:*"
|
|
215831
215832
|
},
|
|
215832
215833
|
peerDependencies: {
|
|
215833
215834
|
"lecodes-design": "^0.8.1",
|
|
215834
|
-
"lecodes-renderer": "^0.8.
|
|
215835
|
+
"lecodes-renderer": "^0.8.5",
|
|
215835
215836
|
"lecodes-3d-editor": "^0.3.0",
|
|
215836
215837
|
"lecodes-assets": "^0.1.0"
|
|
215837
215838
|
},
|
|
@@ -215891,8 +215892,8 @@ import { basename as basename6, dirname as dirname12, join as join17, resolve as
|
|
|
215891
215892
|
init_api();
|
|
215892
215893
|
init_config();
|
|
215893
215894
|
init_manifest();
|
|
215894
|
-
import { writeFileSync as
|
|
215895
|
-
import { basename as basename3, resolve } from "node:path";
|
|
215895
|
+
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
215896
|
+
import { basename as basename3, resolve as resolve2 } from "node:path";
|
|
215896
215897
|
|
|
215897
215898
|
// src/compile/collect.ts
|
|
215898
215899
|
init_localFiles();
|
|
@@ -216005,6 +216006,226 @@ init_projectCompile();
|
|
|
216005
216006
|
init_fonts();
|
|
216006
216007
|
init_assetIcons();
|
|
216007
216008
|
init_ignore();
|
|
216009
|
+
|
|
216010
|
+
// src/commands/appShared.ts
|
|
216011
|
+
init_manifest();
|
|
216012
|
+
init_ignore();
|
|
216013
|
+
init_util();
|
|
216014
|
+
import { execFileSync } from "node:child_process";
|
|
216015
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "node:fs";
|
|
216016
|
+
import { homedir as homedir3 } from "node:os";
|
|
216017
|
+
import { dirname as dirname8, join as join12, resolve } from "node:path";
|
|
216018
|
+
var FIRST_PARTY_PLUGINS = {
|
|
216019
|
+
camera: "letary/lecodes-plugins",
|
|
216020
|
+
"qr-scanner": "letary/lecodes-plugins",
|
|
216021
|
+
geolocation: "letary/lecodes-plugins",
|
|
216022
|
+
push: "letary/lecodes-plugins"
|
|
216023
|
+
};
|
|
216024
|
+
var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
|
|
216025
|
+
var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
|
|
216026
|
+
var desktopDefaultViewport = (root) => {
|
|
216027
|
+
const cfg = readAppConfigAt(root);
|
|
216028
|
+
if (!cfg?.desktop || cfg.ios || cfg.android)
|
|
216029
|
+
return null;
|
|
216030
|
+
return {
|
|
216031
|
+
width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
|
|
216032
|
+
height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
|
|
216033
|
+
};
|
|
216034
|
+
};
|
|
216035
|
+
var readJson = (path) => {
|
|
216036
|
+
if (!existsSync10(path))
|
|
216037
|
+
return null;
|
|
216038
|
+
try {
|
|
216039
|
+
return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
|
|
216040
|
+
} catch {
|
|
216041
|
+
throw new CliError(`Invalid JSON in ${path}`);
|
|
216042
|
+
}
|
|
216043
|
+
};
|
|
216044
|
+
var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
|
|
216045
|
+
`);
|
|
216046
|
+
var projectRoot = () => {
|
|
216047
|
+
try {
|
|
216048
|
+
return findProjectRoot(process.cwd());
|
|
216049
|
+
} catch {
|
|
216050
|
+
return process.cwd();
|
|
216051
|
+
}
|
|
216052
|
+
};
|
|
216053
|
+
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
216054
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
|
|
216055
|
+
var runTool = (cwd, cmd, args) => {
|
|
216056
|
+
try {
|
|
216057
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
|
|
216058
|
+
} catch (e) {
|
|
216059
|
+
const err = e;
|
|
216060
|
+
const tail = ((err.stdout ?? "") + `
|
|
216061
|
+
` + (err.stderr ?? "")).split(`
|
|
216062
|
+
`).filter((l) => l.trim()).slice(-25).join(`
|
|
216063
|
+
`);
|
|
216064
|
+
throw new CliError(`${cmd} ${args[0] ?? ""} failed:
|
|
216065
|
+
${tail}`);
|
|
216066
|
+
}
|
|
216067
|
+
};
|
|
216068
|
+
var ensureShellIgnored = (root, dir) => {
|
|
216069
|
+
const ignorePath = join12(root, ".lecodesignore");
|
|
216070
|
+
const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
|
|
216071
|
+
if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
|
|
216072
|
+
return;
|
|
216073
|
+
const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
|
|
216074
|
+
writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
|
|
216075
|
+
`) + `
|
|
216076
|
+
` + comment + `${dir}/
|
|
216077
|
+
`);
|
|
216078
|
+
log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
|
|
216079
|
+
};
|
|
216080
|
+
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
216081
|
+
var detectEngines = (bundleJs) => {
|
|
216082
|
+
const lines = [];
|
|
216083
|
+
for (const line of bundleJs.split(`
|
|
216084
|
+
`)) {
|
|
216085
|
+
if (!line.startsWith("//"))
|
|
216086
|
+
break;
|
|
216087
|
+
lines.push(line);
|
|
216088
|
+
}
|
|
216089
|
+
const header = lines.join(`
|
|
216090
|
+
`);
|
|
216091
|
+
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
|
|
216092
|
+
};
|
|
216093
|
+
var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
|
|
216094
|
+
const uses = detectEngines(bundleJs);
|
|
216095
|
+
const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
|
|
216096
|
+
if (requested === "auto")
|
|
216097
|
+
return { variant: detected, detected };
|
|
216098
|
+
const variant = requested;
|
|
216099
|
+
const missing = [
|
|
216100
|
+
uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
|
|
216101
|
+
uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
|
|
216102
|
+
].filter(Boolean);
|
|
216103
|
+
if (missing.length > 0) {
|
|
216104
|
+
warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
|
|
216105
|
+
}
|
|
216106
|
+
return { variant, detected };
|
|
216107
|
+
};
|
|
216108
|
+
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
216109
|
+
var cmpVersion = (a, b) => {
|
|
216110
|
+
const pa = a.split(".").map(Number);
|
|
216111
|
+
const pb = b.split(".").map(Number);
|
|
216112
|
+
for (let i = 0;i < 3; i++) {
|
|
216113
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
216114
|
+
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
216115
|
+
}
|
|
216116
|
+
return 0;
|
|
216117
|
+
};
|
|
216118
|
+
var fetchLatestSdkVersion = async (repo) => {
|
|
216119
|
+
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
216120
|
+
const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
|
|
216121
|
+
if (!res.ok)
|
|
216122
|
+
throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`);
|
|
216123
|
+
const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
|
|
216124
|
+
if (!VERSION_RE.test(tag))
|
|
216125
|
+
throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`);
|
|
216126
|
+
return tag;
|
|
216127
|
+
};
|
|
216128
|
+
var resolveSdkVersion = async (pin, fallback, opts) => {
|
|
216129
|
+
if (pin !== "latest") {
|
|
216130
|
+
if (!VERSION_RE.test(pin)) {
|
|
216131
|
+
throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`);
|
|
216132
|
+
}
|
|
216133
|
+
if (cmpVersion(pin, opts.minVersion) < 0) {
|
|
216134
|
+
throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`);
|
|
216135
|
+
}
|
|
216136
|
+
return pin;
|
|
216137
|
+
}
|
|
216138
|
+
try {
|
|
216139
|
+
return await fetchLatestSdkVersion(opts.repo);
|
|
216140
|
+
} catch (e) {
|
|
216141
|
+
const kept = fallback ?? opts.minVersion;
|
|
216142
|
+
warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
|
|
216143
|
+
return kept;
|
|
216144
|
+
}
|
|
216145
|
+
};
|
|
216146
|
+
var ensureRepo = (repo, wantTag, lock) => {
|
|
216147
|
+
const override = process.env.LECODES_PLUGINS_DIR;
|
|
216148
|
+
if (override) {
|
|
216149
|
+
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
|
|
216150
|
+
return { dir: resolve(override), tag: "local", commit: "local" };
|
|
216151
|
+
}
|
|
216152
|
+
const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
|
|
216153
|
+
const url = `https://github.com/${repo}.git`;
|
|
216154
|
+
if (!existsSync10(join12(cache, ".git"))) {
|
|
216155
|
+
mkdirSync6(dirname8(cache), { recursive: true });
|
|
216156
|
+
note(`Cloning ${url}…`);
|
|
216157
|
+
git(dirname8(cache), "clone", "--quiet", url, cache);
|
|
216158
|
+
}
|
|
216159
|
+
let tag = lock?.tag ?? wantTag;
|
|
216160
|
+
let commit = lock?.commit;
|
|
216161
|
+
if (commit) {
|
|
216162
|
+
try {
|
|
216163
|
+
git(cache, "cat-file", "-e", `${commit}^{commit}`);
|
|
216164
|
+
} catch {
|
|
216165
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
216166
|
+
}
|
|
216167
|
+
} else {
|
|
216168
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
216169
|
+
if (!tag) {
|
|
216170
|
+
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
|
|
216171
|
+
`)[0] ?? "";
|
|
216172
|
+
if (!tag)
|
|
216173
|
+
throw new CliError(`${repo} has no v* version tags to install from`);
|
|
216174
|
+
}
|
|
216175
|
+
commit = git(cache, "rev-list", "-n", "1", tag);
|
|
216176
|
+
}
|
|
216177
|
+
git(cache, "checkout", "--quiet", "--detach", commit);
|
|
216178
|
+
return { dir: cache, tag: tag ?? "unknown", commit };
|
|
216179
|
+
};
|
|
216180
|
+
var resolvePlugin = (id, spec, lock, platform) => {
|
|
216181
|
+
let repo;
|
|
216182
|
+
let subdir;
|
|
216183
|
+
let wantTag = null;
|
|
216184
|
+
if (spec.startsWith("github:")) {
|
|
216185
|
+
const [repoPart, tagPart] = spec.slice("github:".length).split("@");
|
|
216186
|
+
repo = repoPart;
|
|
216187
|
+
wantTag = tagPart ?? null;
|
|
216188
|
+
subdir = "";
|
|
216189
|
+
} else {
|
|
216190
|
+
repo = FIRST_PARTY_PLUGINS[id];
|
|
216191
|
+
if (!repo) {
|
|
216192
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
|
|
216193
|
+
}
|
|
216194
|
+
subdir = `plugins/${id}`;
|
|
216195
|
+
}
|
|
216196
|
+
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
|
|
216197
|
+
const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
|
|
216198
|
+
const manifest = readJson(join12(dir, "lecodes-plugin.json"));
|
|
216199
|
+
if (!manifest)
|
|
216200
|
+
throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
|
|
216201
|
+
if (manifest.id !== id)
|
|
216202
|
+
throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
|
|
216203
|
+
if (!manifest[platform]) {
|
|
216204
|
+
throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
|
|
216205
|
+
}
|
|
216206
|
+
if (spec.startsWith("github:")) {
|
|
216207
|
+
const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
|
|
216208
|
+
for (const name of names) {
|
|
216209
|
+
if (!name.includes("."))
|
|
216210
|
+
warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
|
|
216211
|
+
}
|
|
216212
|
+
}
|
|
216213
|
+
return {
|
|
216214
|
+
id,
|
|
216215
|
+
dir,
|
|
216216
|
+
manifest,
|
|
216217
|
+
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
216218
|
+
};
|
|
216219
|
+
};
|
|
216220
|
+
var latestPluginTag = (repo) => {
|
|
216221
|
+
try {
|
|
216222
|
+
return ensureRepo(repo, null, undefined).tag;
|
|
216223
|
+
} catch {
|
|
216224
|
+
return null;
|
|
216225
|
+
}
|
|
216226
|
+
};
|
|
216227
|
+
|
|
216228
|
+
// src/commands/compile.ts
|
|
216008
216229
|
init_util();
|
|
216009
216230
|
var compile2 = async (args) => {
|
|
216010
216231
|
await compileImpl(args);
|
|
@@ -216016,7 +216237,7 @@ var compileImpl = async (args, shell) => {
|
|
|
216016
216237
|
if (out && isNullSink(out)) {
|
|
216017
216238
|
note(`Discarded output (${out})`);
|
|
216018
216239
|
} else if (out) {
|
|
216019
|
-
|
|
216240
|
+
writeFileSync8(resolve2(out), js);
|
|
216020
216241
|
note(`Wrote ${out} (${js.length} bytes)`);
|
|
216021
216242
|
} else {
|
|
216022
216243
|
process.stdout.write(js);
|
|
@@ -216027,7 +216248,7 @@ var compileImpl = async (args, shell) => {
|
|
|
216027
216248
|
}
|
|
216028
216249
|
return resources;
|
|
216029
216250
|
};
|
|
216030
|
-
var buildBundle = async (args, shell) => {
|
|
216251
|
+
var buildBundle = async (args, shell, opts = {}) => {
|
|
216031
216252
|
let root;
|
|
216032
216253
|
let manifest;
|
|
216033
216254
|
try {
|
|
@@ -216046,19 +216267,21 @@ var buildBundle = async (args, shell) => {
|
|
|
216046
216267
|
const config = loadConfig();
|
|
216047
216268
|
const apiUrl = requireApiUrl(config);
|
|
216048
216269
|
const token = requireToken(config);
|
|
216270
|
+
const publicUrlFlag = flagStr(args, "public-url");
|
|
216271
|
+
localAssets = !!shell || !!opts.localAssets && publicUrlFlag === undefined && !flagBool(args, "remote-assets");
|
|
216049
216272
|
note("Resolving project assets…");
|
|
216050
216273
|
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
216051
|
-
({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !
|
|
216274
|
+
({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !localAssets }));
|
|
216052
216275
|
name = manifest.name;
|
|
216053
|
-
publicUrl =
|
|
216054
|
-
|
|
216055
|
-
if (
|
|
216056
|
-
const isRemote = buildGlobMatcher(
|
|
216276
|
+
publicUrl = publicUrlFlag ?? apiUrl;
|
|
216277
|
+
const remotePatterns = shell?.remotePatterns ?? (localAssets ? readAppConfigAt(root)?.resources?.remote ?? [] : []);
|
|
216278
|
+
if (localAssets && remotePatterns.length > 0) {
|
|
216279
|
+
const isRemote = buildGlobMatcher(remotePatterns);
|
|
216057
216280
|
for (const e of entries) {
|
|
216058
216281
|
if (e.type !== "resource" || !isRemote(e.path))
|
|
216059
216282
|
continue;
|
|
216060
216283
|
if (!e.fileSrc) {
|
|
216061
|
-
warnErr(`${e.path} matches resources.remote but has no server URL yet — bundling it (push, then re-sync).`);
|
|
216284
|
+
warnErr(`${e.path} matches resources.remote but has no server URL yet — ${shell ? "bundling it (push, then re-sync)" : "reading it from the working tree"}.`);
|
|
216062
216285
|
continue;
|
|
216063
216286
|
}
|
|
216064
216287
|
e.remote = true;
|
|
@@ -216068,13 +216291,16 @@ var buildBundle = async (args, shell) => {
|
|
|
216068
216291
|
const publicUrlFlag = flagStr(args, "public-url");
|
|
216069
216292
|
localAssets = shell ? true : publicUrlFlag === undefined;
|
|
216070
216293
|
publicUrl = publicUrlFlag ?? "";
|
|
216294
|
+
if (opts.localAssets && flagBool(args, "remote-assets")) {
|
|
216295
|
+
warnErr("This is a local project (no server) — assets always load from the working tree.");
|
|
216296
|
+
}
|
|
216071
216297
|
({ entries, warnings } = collectLocalEntries(root, null, ""));
|
|
216072
216298
|
name = basename3(root);
|
|
216073
216299
|
if (shell && shell.remotePatterns.length > 0) {
|
|
216074
216300
|
warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
|
|
216075
216301
|
}
|
|
216076
216302
|
}
|
|
216077
|
-
const fontsHeader = await applyProjectFonts(entries, manifest && !
|
|
216303
|
+
const fontsHeader = await applyProjectFonts(entries, manifest && !localAssets ? { mode: "cdn", root, publicUrl } : {
|
|
216078
216304
|
mode: "vendor",
|
|
216079
216305
|
root,
|
|
216080
216306
|
vendorDir: root,
|
|
@@ -216822,358 +217048,140 @@ let package = Package(
|
|
|
216822
217048
|
name: "LeCodesRuntime",
|
|
216823
217049
|
dependencies: [
|
|
216824
217050
|
${runtimeDeps.join(`,
|
|
216825
|
-
`)},
|
|
216826
|
-
],
|
|
216827
|
-
path: "Sources/LeCodesRuntime"
|
|
216828
|
-
),${pluginTargets}
|
|
216829
|
-
]
|
|
216830
|
-
)
|
|
216831
|
-
`;
|
|
216832
|
-
};
|
|
216833
|
-
var runtimeSwiftTemplate = (plugins, projectUuid) => {
|
|
216834
|
-
const imports = plugins.map((p) => `import ${p.module}
|
|
216835
|
-
`).join("");
|
|
216836
|
-
const identity = !projectUuid ? "" : ` // World identity: this shell IS the project — the engine attributes the boot world so
|
|
216837
|
-
// identity-gated services (push registration) see the app's uuid from its first
|
|
216838
|
-
// synchronous statement. Baked from .lecodes/manifest.json by \`lecodes app sync\`.
|
|
216839
|
-
engine.bootProjectUuid = "${projectUuid}"
|
|
216840
|
-
`;
|
|
216841
|
-
const calls = plugins.map((p) => ` ${p.register}.register(in: engine)
|
|
216842
|
-
`).join("");
|
|
216843
|
-
const body = identity + calls;
|
|
216844
|
-
return `//
|
|
216845
|
-
// LeCodesRuntime.swift
|
|
216846
|
-
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
216847
|
-
//
|
|
216848
|
-
// Re-exports the SDK (so app code needs only \`import LeCodesRuntime\`) and registers
|
|
216849
|
-
// every plugin installed in app.json.
|
|
216850
|
-
//
|
|
216851
|
-
|
|
216852
|
-
@_exported import LeCodesSDK
|
|
216853
|
-
${imports}
|
|
216854
|
-
public func registerBundledPlugins(in engine: LeCodesEngine) {
|
|
216855
|
-
${body === "" ? `
|
|
216856
|
-
` : body}}
|
|
216857
|
-
`;
|
|
216858
|
-
};
|
|
216859
|
-
var IOS_README = `# ios/ — native Xcode shell
|
|
216860
|
-
|
|
216861
|
-
Generated by \`lecodes app init ios\` for the LeCodes project one folder up. The app runs the
|
|
216862
|
-
compiled bundle in \`App/Resources/app.js\` — fully offline, nothing fetched at launch. This
|
|
216863
|
-
folder is ignored by the platform (\`ios/\` in \`.lecodesignore\`) and is never pushed to LeCodes.
|
|
216864
|
-
|
|
216865
|
-
## Daily workflow
|
|
216866
|
-
|
|
216867
|
-
Edit the app's code at the project root (\`main.ts\` …), then:
|
|
216868
|
-
|
|
216869
|
-
\`\`\`
|
|
216870
|
-
lecodes app sync # recompile app.js + assets into the shell, vendor plugins
|
|
216871
|
-
lecodes app open # open App.xcodeproj in Xcode — build & run from there
|
|
216872
|
-
\`\`\`
|
|
216873
|
-
|
|
216874
|
-
\`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
|
|
216875
|
-
.ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
|
|
216876
|
-
|
|
216877
|
-
## SDK version
|
|
216878
|
-
|
|
216879
|
-
\`../app.json\` \`ios.sdk\` is the exact \`lecodes-ios-sdk\` release this shell links —
|
|
216880
|
-
\`lecodes app init\` pinned the newest one at the time, so builds stay reproducible.
|
|
216881
|
-
|
|
216882
|
-
\`\`\`
|
|
216883
|
-
lecodes app update # → newest SDK release + newest plugin tags, then sync
|
|
216884
|
-
lecodes app update --check # what's available, writes nothing
|
|
216885
|
-
lecodes app update --sdk 1.3.0
|
|
216886
|
-
\`\`\`
|
|
216887
|
-
|
|
216888
|
-
Set \`"sdk": "latest"\` instead of a version to re-resolve on every sync (needs network;
|
|
216889
|
-
offline it keeps whatever the shell already links). The pin is exact rather than a range
|
|
216890
|
-
because the CLI also downloads that release's filament resources bundle into
|
|
216891
|
-
\`App/Resources/\` — the two halves of one SDK must not drift apart.
|
|
216892
|
-
|
|
216893
|
-
## Who owns what
|
|
216894
|
-
|
|
216895
|
-
Yours — \`lecodes app sync\` never touches these:
|
|
216896
|
-
|
|
216897
|
-
- \`App/*.swift\` (AppDelegate, SceneDelegate, ViewController) — customize freely, add files.
|
|
216898
|
-
App-local native code goes here too: add a Swift file that calls
|
|
216899
|
-
\`engine.registerCallback\` / \`registerService\`, and invoke it from ViewController.
|
|
216900
|
-
- \`App/Assets.xcassets/\` — the app icon. Easy path: set \`app.json\` \`"icon"\` to a square
|
|
216901
|
-
≥1024px image and every sync renders AppIcon.png from it (flattened over \`"iconBackground"\`,
|
|
216902
|
-
default white — App Store icons must be opaque). Or leave the key unset and drop a
|
|
216903
|
-
1024×1024 PNG into AppIcon.appiconset/ yourself — sync never touches it then.
|
|
216904
|
-
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
216905
|
-
|
|
216906
|
-
CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
216907
|
-
|
|
216908
|
-
- \`App/Resources/\` — the compiled app.js + every asset it references (fonts included), so
|
|
216909
|
-
the app runs fully offline. Globs in app.json \`"resources": {"remote": [...]}\` opt heavy
|
|
216910
|
-
assets (videos, big models) back into streaming from the server instead.
|
|
216911
|
-
- \`App/Info.plist\` — template + the installed plugins' usage keys
|
|
216912
|
-
- \`App/App.entitlements\` — the capabilities the installed plugins declare (e.g. push)
|
|
216913
|
-
- \`LeCodesRuntime/\` — local Swift package: the LeCodesSDK dependency, vendored plugin
|
|
216914
|
-
sources (Sources/<Module>/), and the registerBundledPlugins entry point
|
|
216915
|
-
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
216916
|
-
- this README
|
|
216917
|
-
|
|
216918
|
-
## SDK variant
|
|
216919
|
-
|
|
216920
|
-
Sync links the smallest SDK product that covers what the compiled bundle actually uses:
|
|
216921
|
-
\`core\` (UI only), \`2d\` (+ 2D engine with physics), \`3d\` (+ 3D/AR engine with physics,
|
|
216922
|
-
~17 MB heavier), or \`full\` (everything). Adding your first 3D scene or 2D game to the
|
|
216923
|
-
project switches it automatically on the next sync; set \`ios.variant\` in \`../app.json\`
|
|
216924
|
-
to pin one instead.
|
|
216925
|
-
|
|
216926
|
-
## Over-the-air updates
|
|
216927
|
-
|
|
216928
|
-
With \`"update": true\` in \`../app.json\` (server-backed projects; or
|
|
216929
|
-
\`"update": {"url": "https://…/app.js"}\` for any static hosting), the app checks that URL
|
|
216930
|
-
at launch — natively, before any JS runs, so a broken bundle can never block its own fix.
|
|
216931
|
-
Unchanged code costs a ~0-byte 304; new code downloads in the background and runs on the
|
|
216932
|
-
NEXT launch. Offline it's a silent no-op — the app always launches from a local bundle.
|
|
216933
|
-
An App Store update always beats a cached OTA bundle. JS only: adding/removing native
|
|
216934
|
-
plugins still needs a store release.
|
|
216935
|
-
|
|
216936
|
-
Shipping an update is just \`lecodes push --publish\` — no rebuild, no store review:
|
|
216937
|
-
running apps download the new code on their next launch and run it on the launch after.
|
|
216938
|
-
|
|
216939
|
-
Shells generated before OTA support have a \`ViewController.swift\` that reads app.js from
|
|
216940
|
-
\`Bundle.main\` directly, so downloaded updates would never run — and since \`App/*.swift\`
|
|
216941
|
-
is yours, sync won't rewrite it (\`lecodes app doctor\` flags this). Replace the
|
|
216942
|
-
\`Bundle.main\` load with:
|
|
216943
|
-
|
|
216944
|
-
\`\`\`swift
|
|
216945
|
-
LeCodesUpdater.checkForUpdate()
|
|
216946
|
-
guard let code = LeCodesUpdater.activeBundle() else {
|
|
216947
|
-
print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
|
|
216948
|
-
return
|
|
216949
|
-
}
|
|
216950
|
-
engine.run(code)
|
|
216951
|
-
\`\`\`
|
|
216952
|
-
|
|
216953
|
-
## Config
|
|
216954
|
-
|
|
216955
|
-
Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
|
|
216956
|
-
buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are managed with
|
|
216957
|
-
\`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
|
|
216958
|
-
`;
|
|
216959
|
-
|
|
216960
|
-
// src/commands/appShared.ts
|
|
216961
|
-
init_manifest();
|
|
216962
|
-
init_ignore();
|
|
216963
|
-
init_util();
|
|
216964
|
-
import { execFileSync } from "node:child_process";
|
|
216965
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
216966
|
-
import { homedir as homedir3 } from "node:os";
|
|
216967
|
-
import { dirname as dirname8, join as join12, resolve as resolve2 } from "node:path";
|
|
216968
|
-
var FIRST_PARTY_PLUGINS = {
|
|
216969
|
-
camera: "letary/lecodes-plugins",
|
|
216970
|
-
"qr-scanner": "letary/lecodes-plugins",
|
|
216971
|
-
geolocation: "letary/lecodes-plugins",
|
|
216972
|
-
push: "letary/lecodes-plugins"
|
|
216973
|
-
};
|
|
216974
|
-
var readAppConfigAt = (root) => readJson(join12(root, "app.json"));
|
|
216975
|
-
var DESKTOP_DEFAULT_WINDOW = { width: 1280, height: 720 };
|
|
216976
|
-
var desktopDefaultViewport = (root) => {
|
|
216977
|
-
const cfg = readAppConfigAt(root);
|
|
216978
|
-
if (!cfg?.desktop || cfg.ios || cfg.android)
|
|
216979
|
-
return null;
|
|
216980
|
-
return {
|
|
216981
|
-
width: cfg.desktop.window?.width ?? DESKTOP_DEFAULT_WINDOW.width,
|
|
216982
|
-
height: cfg.desktop.window?.height ?? DESKTOP_DEFAULT_WINDOW.height
|
|
216983
|
-
};
|
|
216984
|
-
};
|
|
216985
|
-
var readJson = (path) => {
|
|
216986
|
-
if (!existsSync10(path))
|
|
216987
|
-
return null;
|
|
216988
|
-
try {
|
|
216989
|
-
return JSON.parse(readFileSync11(path, "utf8").replace(/^\uFEFF/, ""));
|
|
216990
|
-
} catch {
|
|
216991
|
-
throw new CliError(`Invalid JSON in ${path}`);
|
|
216992
|
-
}
|
|
216993
|
-
};
|
|
216994
|
-
var writeJson = (path, value) => writeFileSync8(path, JSON.stringify(value, null, 2) + `
|
|
216995
|
-
`);
|
|
216996
|
-
var projectRoot = () => {
|
|
216997
|
-
try {
|
|
216998
|
-
return findProjectRoot(process.cwd());
|
|
216999
|
-
} catch {
|
|
217000
|
-
return process.cwd();
|
|
217001
|
-
}
|
|
217002
|
-
};
|
|
217003
|
-
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
217004
|
-
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
|
|
217005
|
-
var runTool = (cwd, cmd, args) => {
|
|
217006
|
-
try {
|
|
217007
|
-
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
|
|
217008
|
-
} catch (e) {
|
|
217009
|
-
const err = e;
|
|
217010
|
-
const tail = ((err.stdout ?? "") + `
|
|
217011
|
-
` + (err.stderr ?? "")).split(`
|
|
217012
|
-
`).filter((l) => l.trim()).slice(-25).join(`
|
|
217013
|
-
`);
|
|
217014
|
-
throw new CliError(`${cmd} ${args[0] ?? ""} failed:
|
|
217015
|
-
${tail}`);
|
|
217016
|
-
}
|
|
217017
|
-
};
|
|
217018
|
-
var ensureShellIgnored = (root, dir) => {
|
|
217019
|
-
const ignorePath = join12(root, ".lecodesignore");
|
|
217020
|
-
const ignore = existsSync10(ignorePath) ? readFileSync11(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
|
|
217021
|
-
if (new RegExp(`^${dir}\\/$`, "m").test(ignore))
|
|
217022
|
-
return;
|
|
217023
|
-
const comment = ignore.includes("# Native app shells") ? "" : "# Native app shells — generated by `lecodes app init`, never pushed to LeCodes.\n";
|
|
217024
|
-
writeFileSync8(ignorePath, ignore.replace(/\n*$/, `
|
|
217025
|
-
`) + `
|
|
217026
|
-
` + comment + `${dir}/
|
|
217027
|
-
`);
|
|
217028
|
-
log(`${c.green("updated")} .lecodesignore (+ ${dir}/)`);
|
|
217029
|
-
};
|
|
217030
|
-
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
217031
|
-
var detectEngines = (bundleJs) => {
|
|
217032
|
-
const lines = [];
|
|
217033
|
-
for (const line of bundleJs.split(`
|
|
217034
|
-
`)) {
|
|
217035
|
-
if (!line.startsWith("//"))
|
|
217036
|
-
break;
|
|
217037
|
-
lines.push(line);
|
|
217038
|
-
}
|
|
217039
|
-
const header = lines.join(`
|
|
217040
|
-
`);
|
|
217041
|
-
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
|
|
217042
|
-
};
|
|
217043
|
-
var resolveVariant = (requested, bundleJs, configKey = "ios.variant") => {
|
|
217044
|
-
const uses = detectEngines(bundleJs);
|
|
217045
|
-
const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
|
|
217046
|
-
if (requested === "auto")
|
|
217047
|
-
return { variant: detected, detected };
|
|
217048
|
-
const variant = requested;
|
|
217049
|
-
const missing = [
|
|
217050
|
-
uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
|
|
217051
|
-
uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
|
|
217052
|
-
].filter(Boolean);
|
|
217053
|
-
if (missing.length > 0) {
|
|
217054
|
-
warn(`project uses the ${missing.join(" + ")} engine but ${configKey} forces "${variant}" — those scenes won't run on device; remove ${configKey} to auto-select "${detected}".`);
|
|
217055
|
-
}
|
|
217056
|
-
return { variant, detected };
|
|
217057
|
-
};
|
|
217058
|
-
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
217059
|
-
var cmpVersion = (a, b) => {
|
|
217060
|
-
const pa = a.split(".").map(Number);
|
|
217061
|
-
const pb = b.split(".").map(Number);
|
|
217062
|
-
for (let i = 0;i < 3; i++) {
|
|
217063
|
-
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
217064
|
-
return (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
217065
|
-
}
|
|
217066
|
-
return 0;
|
|
217067
|
-
};
|
|
217068
|
-
var fetchLatestSdkVersion = async (repo) => {
|
|
217069
|
-
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
217070
|
-
const res = await fetch(url, { headers: { accept: "application/vnd.github+json", "user-agent": "lecodes-cli" } });
|
|
217071
|
-
if (!res.ok)
|
|
217072
|
-
throw new CliError(`Could not read the latest ${repo} release (HTTP ${res.status}) — ${url}`);
|
|
217073
|
-
const tag = (await res.json()).tag_name?.replace(/^v/, "") ?? "";
|
|
217074
|
-
if (!VERSION_RE.test(tag))
|
|
217075
|
-
throw new CliError(`${repo} latest release has no x.y.z tag (got "${tag}")`);
|
|
217076
|
-
return tag;
|
|
217077
|
-
};
|
|
217078
|
-
var resolveSdkVersion = async (pin, fallback, opts) => {
|
|
217079
|
-
if (pin !== "latest") {
|
|
217080
|
-
if (!VERSION_RE.test(pin)) {
|
|
217081
|
-
throw new CliError(`app.json ${opts.configKey} must be "latest" or an exact x.y.z release (got "${pin}").`);
|
|
217082
|
-
}
|
|
217083
|
-
if (cmpVersion(pin, opts.minVersion) < 0) {
|
|
217084
|
-
throw new CliError(`app.json ${opts.configKey} ${pin} is older than the ${opts.minVersion} these generated shells need.`);
|
|
217085
|
-
}
|
|
217086
|
-
return pin;
|
|
217087
|
-
}
|
|
217088
|
-
try {
|
|
217089
|
-
return await fetchLatestSdkVersion(opts.repo);
|
|
217090
|
-
} catch (e) {
|
|
217091
|
-
const kept = fallback ?? opts.minVersion;
|
|
217092
|
-
warn(`Could not resolve the latest SDK (${e.message}) — staying on ${kept}.`);
|
|
217093
|
-
return kept;
|
|
217094
|
-
}
|
|
217095
|
-
};
|
|
217096
|
-
var ensureRepo = (repo, wantTag, lock) => {
|
|
217097
|
-
const override = process.env.LECODES_PLUGINS_DIR;
|
|
217098
|
-
if (override) {
|
|
217099
|
-
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
|
|
217100
|
-
return { dir: resolve2(override), tag: "local", commit: "local" };
|
|
217101
|
-
}
|
|
217102
|
-
const cache = join12(homedir3(), ".lecodes", "plugins", repo.replace("/", "__"));
|
|
217103
|
-
const url = `https://github.com/${repo}.git`;
|
|
217104
|
-
if (!existsSync10(join12(cache, ".git"))) {
|
|
217105
|
-
mkdirSync6(dirname8(cache), { recursive: true });
|
|
217106
|
-
note(`Cloning ${url}…`);
|
|
217107
|
-
git(dirname8(cache), "clone", "--quiet", url, cache);
|
|
217108
|
-
}
|
|
217109
|
-
let tag = lock?.tag ?? wantTag;
|
|
217110
|
-
let commit = lock?.commit;
|
|
217111
|
-
if (commit) {
|
|
217112
|
-
try {
|
|
217113
|
-
git(cache, "cat-file", "-e", `${commit}^{commit}`);
|
|
217114
|
-
} catch {
|
|
217115
|
-
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
217116
|
-
}
|
|
217117
|
-
} else {
|
|
217118
|
-
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
217119
|
-
if (!tag) {
|
|
217120
|
-
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
|
|
217121
|
-
`)[0] ?? "";
|
|
217122
|
-
if (!tag)
|
|
217123
|
-
throw new CliError(`${repo} has no v* version tags to install from`);
|
|
217124
|
-
}
|
|
217125
|
-
commit = git(cache, "rev-list", "-n", "1", tag);
|
|
217126
|
-
}
|
|
217127
|
-
git(cache, "checkout", "--quiet", "--detach", commit);
|
|
217128
|
-
return { dir: cache, tag: tag ?? "unknown", commit };
|
|
217129
|
-
};
|
|
217130
|
-
var resolvePlugin = (id, spec, lock, platform) => {
|
|
217131
|
-
let repo;
|
|
217132
|
-
let subdir;
|
|
217133
|
-
let wantTag = null;
|
|
217134
|
-
if (spec.startsWith("github:")) {
|
|
217135
|
-
const [repoPart, tagPart] = spec.slice("github:".length).split("@");
|
|
217136
|
-
repo = repoPart;
|
|
217137
|
-
wantTag = tagPart ?? null;
|
|
217138
|
-
subdir = "";
|
|
217139
|
-
} else {
|
|
217140
|
-
repo = FIRST_PARTY_PLUGINS[id];
|
|
217141
|
-
if (!repo) {
|
|
217142
|
-
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
|
|
217143
|
-
}
|
|
217144
|
-
subdir = `plugins/${id}`;
|
|
217145
|
-
}
|
|
217146
|
-
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
|
|
217147
|
-
const dir = subdir ? join12(checkout.dir, subdir) : checkout.dir;
|
|
217148
|
-
const manifest = readJson(join12(dir, "lecodes-plugin.json"));
|
|
217149
|
-
if (!manifest)
|
|
217150
|
-
throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
|
|
217151
|
-
if (manifest.id !== id)
|
|
217152
|
-
throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
|
|
217153
|
-
if (!manifest[platform]) {
|
|
217154
|
-
throw new CliError(`Plugin "${id}" does not support ${platform === "ios" ? "iOS" : "Android"} (manifest "${platform}" is null)`);
|
|
217155
|
-
}
|
|
217156
|
-
if (spec.startsWith("github:")) {
|
|
217157
|
-
const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
|
|
217158
|
-
for (const name of names) {
|
|
217159
|
-
if (!name.includes("."))
|
|
217160
|
-
warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
|
|
217161
|
-
}
|
|
217162
|
-
}
|
|
217163
|
-
return {
|
|
217164
|
-
id,
|
|
217165
|
-
dir,
|
|
217166
|
-
manifest,
|
|
217167
|
-
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
217168
|
-
};
|
|
217051
|
+
`)},
|
|
217052
|
+
],
|
|
217053
|
+
path: "Sources/LeCodesRuntime"
|
|
217054
|
+
),${pluginTargets}
|
|
217055
|
+
]
|
|
217056
|
+
)
|
|
217057
|
+
`;
|
|
217169
217058
|
};
|
|
217170
|
-
var
|
|
217171
|
-
|
|
217172
|
-
|
|
217173
|
-
|
|
217174
|
-
|
|
217175
|
-
|
|
217059
|
+
var runtimeSwiftTemplate = (plugins, projectUuid) => {
|
|
217060
|
+
const imports = plugins.map((p) => `import ${p.module}
|
|
217061
|
+
`).join("");
|
|
217062
|
+
const identity = !projectUuid ? "" : ` // World identity: this shell IS the project — the engine attributes the boot world so
|
|
217063
|
+
// identity-gated services (push registration) see the app's uuid from its first
|
|
217064
|
+
// synchronous statement. Baked from .lecodes/manifest.json by \`lecodes app sync\`.
|
|
217065
|
+
engine.bootProjectUuid = "${projectUuid}"
|
|
217066
|
+
`;
|
|
217067
|
+
const calls = plugins.map((p) => ` ${p.register}.register(in: engine)
|
|
217068
|
+
`).join("");
|
|
217069
|
+
const body = identity + calls;
|
|
217070
|
+
return `//
|
|
217071
|
+
// LeCodesRuntime.swift
|
|
217072
|
+
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
217073
|
+
//
|
|
217074
|
+
// Re-exports the SDK (so app code needs only \`import LeCodesRuntime\`) and registers
|
|
217075
|
+
// every plugin installed in app.json.
|
|
217076
|
+
//
|
|
217077
|
+
|
|
217078
|
+
@_exported import LeCodesSDK
|
|
217079
|
+
${imports}
|
|
217080
|
+
public func registerBundledPlugins(in engine: LeCodesEngine) {
|
|
217081
|
+
${body === "" ? `
|
|
217082
|
+
` : body}}
|
|
217083
|
+
`;
|
|
217176
217084
|
};
|
|
217085
|
+
var IOS_README = `# ios/ — native Xcode shell
|
|
217086
|
+
|
|
217087
|
+
Generated by \`lecodes app init ios\` for the LeCodes project one folder up. The app runs the
|
|
217088
|
+
compiled bundle in \`App/Resources/app.js\` — fully offline, nothing fetched at launch. This
|
|
217089
|
+
folder is ignored by the platform (\`ios/\` in \`.lecodesignore\`) and is never pushed to LeCodes.
|
|
217090
|
+
|
|
217091
|
+
## Daily workflow
|
|
217092
|
+
|
|
217093
|
+
Edit the app's code at the project root (\`main.ts\` …), then:
|
|
217094
|
+
|
|
217095
|
+
\`\`\`
|
|
217096
|
+
lecodes app sync # recompile app.js + assets into the shell, vendor plugins
|
|
217097
|
+
lecodes app open # open App.xcodeproj in Xcode — build & run from there
|
|
217098
|
+
\`\`\`
|
|
217099
|
+
|
|
217100
|
+
\`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
|
|
217101
|
+
.ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
|
|
217102
|
+
|
|
217103
|
+
## SDK version
|
|
217104
|
+
|
|
217105
|
+
\`../app.json\` \`ios.sdk\` is the exact \`lecodes-ios-sdk\` release this shell links —
|
|
217106
|
+
\`lecodes app init\` pinned the newest one at the time, so builds stay reproducible.
|
|
217107
|
+
|
|
217108
|
+
\`\`\`
|
|
217109
|
+
lecodes app update # → newest SDK release + newest plugin tags, then sync
|
|
217110
|
+
lecodes app update --check # what's available, writes nothing
|
|
217111
|
+
lecodes app update --sdk 1.3.0
|
|
217112
|
+
\`\`\`
|
|
217113
|
+
|
|
217114
|
+
Set \`"sdk": "latest"\` instead of a version to re-resolve on every sync (needs network;
|
|
217115
|
+
offline it keeps whatever the shell already links). The pin is exact rather than a range
|
|
217116
|
+
because the CLI also downloads that release's filament resources bundle into
|
|
217117
|
+
\`App/Resources/\` — the two halves of one SDK must not drift apart.
|
|
217118
|
+
|
|
217119
|
+
## Who owns what
|
|
217120
|
+
|
|
217121
|
+
Yours — \`lecodes app sync\` never touches these:
|
|
217122
|
+
|
|
217123
|
+
- \`App/*.swift\` (AppDelegate, SceneDelegate, ViewController) — customize freely, add files.
|
|
217124
|
+
App-local native code goes here too: add a Swift file that calls
|
|
217125
|
+
\`engine.registerCallback\` / \`registerService\`, and invoke it from ViewController.
|
|
217126
|
+
- \`App/Assets.xcassets/\` — the app icon. Easy path: set \`app.json\` \`"icon"\` to a square
|
|
217127
|
+
≥1024px image and every sync renders AppIcon.png from it (flattened over \`"iconBackground"\`,
|
|
217128
|
+
default white — App Store icons must be opaque). Or leave the key unset and drop a
|
|
217129
|
+
1024×1024 PNG into AppIcon.appiconset/ yourself — sync never touches it then.
|
|
217130
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
217131
|
+
|
|
217132
|
+
CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
217133
|
+
|
|
217134
|
+
- \`App/Resources/\` — the compiled app.js + every asset it references (fonts included), so
|
|
217135
|
+
the app runs fully offline. Globs in app.json \`"resources": {"remote": [...]}\` opt heavy
|
|
217136
|
+
assets (videos, big models) back into streaming from the server instead.
|
|
217137
|
+
- \`App/Info.plist\` — template + the installed plugins' usage keys
|
|
217138
|
+
- \`App/App.entitlements\` — the capabilities the installed plugins declare (e.g. push)
|
|
217139
|
+
- \`LeCodesRuntime/\` — local Swift package: the LeCodesSDK dependency, vendored plugin
|
|
217140
|
+
sources (Sources/<Module>/), and the registerBundledPlugins entry point
|
|
217141
|
+
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
217142
|
+
- this README
|
|
217143
|
+
|
|
217144
|
+
## SDK variant
|
|
217145
|
+
|
|
217146
|
+
Sync links the smallest SDK product that covers what the compiled bundle actually uses:
|
|
217147
|
+
\`core\` (UI only), \`2d\` (+ 2D engine with physics), \`3d\` (+ 3D/AR engine with physics,
|
|
217148
|
+
~17 MB heavier), or \`full\` (everything). Adding your first 3D scene or 2D game to the
|
|
217149
|
+
project switches it automatically on the next sync; set \`ios.variant\` in \`../app.json\`
|
|
217150
|
+
to pin one instead.
|
|
217151
|
+
|
|
217152
|
+
## Over-the-air updates
|
|
217153
|
+
|
|
217154
|
+
With \`"update": true\` in \`../app.json\` (server-backed projects; or
|
|
217155
|
+
\`"update": {"url": "https://…/app.js"}\` for any static hosting), the app checks that URL
|
|
217156
|
+
at launch — natively, before any JS runs, so a broken bundle can never block its own fix.
|
|
217157
|
+
Unchanged code costs a ~0-byte 304; new code downloads in the background and runs on the
|
|
217158
|
+
NEXT launch. Offline it's a silent no-op — the app always launches from a local bundle.
|
|
217159
|
+
An App Store update always beats a cached OTA bundle. JS only: adding/removing native
|
|
217160
|
+
plugins still needs a store release.
|
|
217161
|
+
|
|
217162
|
+
Shipping an update is just \`lecodes push --publish\` — no rebuild, no store review:
|
|
217163
|
+
running apps download the new code on their next launch and run it on the launch after.
|
|
217164
|
+
|
|
217165
|
+
Shells generated before OTA support have a \`ViewController.swift\` that reads app.js from
|
|
217166
|
+
\`Bundle.main\` directly, so downloaded updates would never run — and since \`App/*.swift\`
|
|
217167
|
+
is yours, sync won't rewrite it (\`lecodes app doctor\` flags this). Replace the
|
|
217168
|
+
\`Bundle.main\` load with:
|
|
217169
|
+
|
|
217170
|
+
\`\`\`swift
|
|
217171
|
+
LeCodesUpdater.checkForUpdate()
|
|
217172
|
+
guard let code = LeCodesUpdater.activeBundle() else {
|
|
217173
|
+
print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
|
|
217174
|
+
return
|
|
217175
|
+
}
|
|
217176
|
+
engine.run(code)
|
|
217177
|
+
\`\`\`
|
|
217178
|
+
|
|
217179
|
+
## Config
|
|
217180
|
+
|
|
217181
|
+
Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
|
|
217182
|
+
buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are managed with
|
|
217183
|
+
\`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
|
|
217184
|
+
`;
|
|
217177
217185
|
|
|
217178
217186
|
// src/commands/appAndroid.ts
|
|
217179
217187
|
init_manifest();
|
|
@@ -218692,6 +218700,106 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
|
218692
218700
|
import { copyFileSync, existsSync as existsSync14, mkdirSync as mkdirSync9, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync3, writeFileSync as writeFileSync11 } from "node:fs";
|
|
218693
218701
|
import { homedir as homedir5, tmpdir } from "node:os";
|
|
218694
218702
|
import { join as join15, resolve as resolve4 } from "node:path";
|
|
218703
|
+
|
|
218704
|
+
// ../viewer-lite/src/sourcemapError.ts
|
|
218705
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
218706
|
+
var B64_INDEX = {};
|
|
218707
|
+
for (let i2 = 0;i2 < B64.length; i2++)
|
|
218708
|
+
B64_INDEX[B64[i2]] = i2;
|
|
218709
|
+
var decodeVlq = (segment) => {
|
|
218710
|
+
const out = [];
|
|
218711
|
+
let shift = 0, value = 0;
|
|
218712
|
+
for (const ch of segment) {
|
|
218713
|
+
const digit = B64_INDEX[ch];
|
|
218714
|
+
if (digit === undefined)
|
|
218715
|
+
break;
|
|
218716
|
+
const more = digit & 32;
|
|
218717
|
+
value += (digit & 31) << shift;
|
|
218718
|
+
if (more) {
|
|
218719
|
+
shift += 5;
|
|
218720
|
+
} else {
|
|
218721
|
+
const negative = value & 1;
|
|
218722
|
+
value >>= 1;
|
|
218723
|
+
out.push(negative ? -value : value);
|
|
218724
|
+
value = 0;
|
|
218725
|
+
shift = 0;
|
|
218726
|
+
}
|
|
218727
|
+
}
|
|
218728
|
+
return out;
|
|
218729
|
+
};
|
|
218730
|
+
|
|
218731
|
+
class SourceMap {
|
|
218732
|
+
sources;
|
|
218733
|
+
lines;
|
|
218734
|
+
constructor(raw) {
|
|
218735
|
+
this.sources = raw.sources;
|
|
218736
|
+
this.lines = [];
|
|
218737
|
+
let srcIdx = 0, srcLine = 0, srcCol = 0;
|
|
218738
|
+
for (const lineStr of raw.mappings.split(";")) {
|
|
218739
|
+
const segs = [];
|
|
218740
|
+
let genCol = 0;
|
|
218741
|
+
if (lineStr) {
|
|
218742
|
+
for (const segStr of lineStr.split(",")) {
|
|
218743
|
+
const v = decodeVlq(segStr);
|
|
218744
|
+
if (v.length === 0)
|
|
218745
|
+
continue;
|
|
218746
|
+
genCol += v[0];
|
|
218747
|
+
if (v.length >= 4) {
|
|
218748
|
+
srcIdx += v[1];
|
|
218749
|
+
srcLine += v[2];
|
|
218750
|
+
srcCol += v[3];
|
|
218751
|
+
segs.push([genCol, srcIdx, srcLine, srcCol]);
|
|
218752
|
+
}
|
|
218753
|
+
}
|
|
218754
|
+
}
|
|
218755
|
+
this.lines.push(segs);
|
|
218756
|
+
}
|
|
218757
|
+
}
|
|
218758
|
+
originalPositionFor(line, column) {
|
|
218759
|
+
const segs = this.lines[line - 1];
|
|
218760
|
+
if (!segs || segs.length === 0)
|
|
218761
|
+
return null;
|
|
218762
|
+
let best = null;
|
|
218763
|
+
for (const seg of segs) {
|
|
218764
|
+
if (seg[0] <= column)
|
|
218765
|
+
best = seg;
|
|
218766
|
+
else
|
|
218767
|
+
break;
|
|
218768
|
+
}
|
|
218769
|
+
best ??= segs[0];
|
|
218770
|
+
const source = this.sources[best[1]];
|
|
218771
|
+
if (source === undefined)
|
|
218772
|
+
return null;
|
|
218773
|
+
return { source, line: best[2] + 1, column: best[3] };
|
|
218774
|
+
}
|
|
218775
|
+
}
|
|
218776
|
+
var extractInlineSourceMap = (code) => {
|
|
218777
|
+
const m = code.match(/\/\/# lecodesMap=data:application\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)\s*$/);
|
|
218778
|
+
if (!m)
|
|
218779
|
+
return null;
|
|
218780
|
+
try {
|
|
218781
|
+
const json = typeof atob === "function" ? atob(m[1]) : Buffer.from(m[1], "base64").toString("utf8");
|
|
218782
|
+
return new SourceMap(JSON.parse(json));
|
|
218783
|
+
} catch {
|
|
218784
|
+
return null;
|
|
218785
|
+
}
|
|
218786
|
+
};
|
|
218787
|
+
var mapNamedFileStack = (text, map, filename) => {
|
|
218788
|
+
const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
218789
|
+
return text.replace(new RegExp(`\\b${escaped}:(\\d+):(\\d+)`, "g"), (whole, l2, c4) => {
|
|
218790
|
+
const orig = map.originalPositionFor(parseInt(l2, 10), Math.max(0, parseInt(c4, 10) - 1));
|
|
218791
|
+
return orig ? `${orig.source.replace(/^(\.\/|\/)/, "")}:${orig.line}:${orig.column}` : whole;
|
|
218792
|
+
});
|
|
218793
|
+
};
|
|
218794
|
+
|
|
218795
|
+
// src/compile/nativeStack.ts
|
|
218796
|
+
var BUNDLE_FILENAME = "module.js";
|
|
218797
|
+
var makeStackDemangler = (bundleJs) => {
|
|
218798
|
+
const map = extractInlineSourceMap(bundleJs);
|
|
218799
|
+
return map ? (chunk) => mapNamedFileStack(chunk, map, BUNDLE_FILENAME) : null;
|
|
218800
|
+
};
|
|
218801
|
+
|
|
218802
|
+
// src/desktopRenderer.ts
|
|
218695
218803
|
var MANIFEST_URL = "https://storage.yandexcloud.net/le-codes/releases/versions.json";
|
|
218696
218804
|
var DESKTOP_RENDERER_VERSION = "1.1.0";
|
|
218697
218805
|
var semverNewer = (a2, b) => {
|
|
@@ -218864,13 +218972,34 @@ var stageBundle = (js, resources) => {
|
|
|
218864
218972
|
}
|
|
218865
218973
|
return runDir;
|
|
218866
218974
|
};
|
|
218975
|
+
var pipeThroughDemangler = (src, dst, map) => {
|
|
218976
|
+
let rest = "";
|
|
218977
|
+
src.on("data", (d) => {
|
|
218978
|
+
rest += String(d);
|
|
218979
|
+
const nl = rest.lastIndexOf(`
|
|
218980
|
+
`);
|
|
218981
|
+
if (nl < 0)
|
|
218982
|
+
return;
|
|
218983
|
+
dst.write(map(rest.slice(0, nl + 1)));
|
|
218984
|
+
rest = rest.slice(nl + 1);
|
|
218985
|
+
});
|
|
218986
|
+
src.on("end", () => {
|
|
218987
|
+
if (rest)
|
|
218988
|
+
dst.write(map(rest));
|
|
218989
|
+
});
|
|
218990
|
+
};
|
|
218867
218991
|
var runDesktopApp = async (js, resources, opts = {}) => {
|
|
218868
218992
|
const exe = await resolveDesktopExe();
|
|
218869
218993
|
const runDir = stageBundle(js, resources);
|
|
218870
218994
|
try {
|
|
218871
218995
|
const env = { ...process.env, ...windowEnv(opts) };
|
|
218996
|
+
const demangle = makeStackDemangler(js);
|
|
218872
218997
|
await new Promise((done, fail) => {
|
|
218873
|
-
const child = spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
|
|
218998
|
+
const child = demangle ? spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] }) : spawn2(exe, [join15(runDir, "app.js")], { cwd: runDir, env, stdio: ["ignore", "inherit", "inherit"] });
|
|
218999
|
+
if (demangle) {
|
|
219000
|
+
pipeThroughDemangler(child.stdout, process.stdout, demangle);
|
|
219001
|
+
pipeThroughDemangler(child.stderr, process.stderr, demangle);
|
|
219002
|
+
}
|
|
218874
219003
|
child.on("error", (e2) => fail(new CliError(`Can't run the desktop host: ${e2.message}`)));
|
|
218875
219004
|
child.on("exit", (code) => {
|
|
218876
219005
|
if (code === 0 || code === null)
|
|
@@ -218898,26 +219027,33 @@ var runDesktopRender = async (js, resources, opts) => {
|
|
|
218898
219027
|
try {
|
|
218899
219028
|
const appJs = join15(runDir, "app.js");
|
|
218900
219029
|
const outAbs = resolve4(opts.outPng);
|
|
218901
|
-
|
|
218902
|
-
|
|
218903
|
-
|
|
218904
|
-
|
|
218905
|
-
|
|
218906
|
-
|
|
218907
|
-
|
|
218908
|
-
|
|
218909
|
-
|
|
218910
|
-
|
|
218911
|
-
|
|
218912
|
-
|
|
218913
|
-
|
|
219030
|
+
const env = {
|
|
219031
|
+
...process.env,
|
|
219032
|
+
CREATOR_DUMP_PNG: outAbs,
|
|
219033
|
+
CREATOR_MAX_FRAMES: String(opts.frames),
|
|
219034
|
+
CREATOR_WINDOW_SIZE: `${opts.width}x${opts.height}`,
|
|
219035
|
+
CREATOR_HIDDEN: "1"
|
|
219036
|
+
};
|
|
219037
|
+
if (opts.script !== undefined) {
|
|
219038
|
+
const scriptPath = join15(runDir, "script.txt");
|
|
219039
|
+
writeFileSync11(scriptPath, opts.script);
|
|
219040
|
+
env.CREATOR_SCRIPT = scriptPath;
|
|
219041
|
+
}
|
|
219042
|
+
if (opts.fixedDtMs)
|
|
219043
|
+
env.CREATOR_FIXED_DT = String(opts.fixedDtMs);
|
|
219044
|
+
if (opts.camera)
|
|
219045
|
+
env.CREATOR_CAMERA = opts.camera;
|
|
219046
|
+
if (opts.view)
|
|
219047
|
+
env.CREATOR_VIEW = opts.view;
|
|
219048
|
+
const demangle = makeStackDemangler(js) ?? ((text) => text);
|
|
219049
|
+
return await new Promise((done, fail) => {
|
|
219050
|
+
const child = spawn2(exe, [appJs], { cwd: runDir, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
219051
|
+
const chunks = [];
|
|
218914
219052
|
const sink = (d) => {
|
|
218915
219053
|
const text = String(d);
|
|
218916
|
-
|
|
218917
|
-
while (tail.length > 80)
|
|
218918
|
-
tail.shift();
|
|
219054
|
+
chunks.push(text);
|
|
218919
219055
|
if (opts.logs)
|
|
218920
|
-
process.stderr.write(text);
|
|
219056
|
+
process.stderr.write(demangle(text));
|
|
218921
219057
|
};
|
|
218922
219058
|
child.stdout.on("data", sink);
|
|
218923
219059
|
child.stderr.on("data", sink);
|
|
@@ -218931,10 +219067,12 @@ var runDesktopRender = async (js, resources, opts) => {
|
|
|
218931
219067
|
});
|
|
218932
219068
|
child.on("exit", (code) => {
|
|
218933
219069
|
clearTimeout(killer);
|
|
219070
|
+
const output = demangle(chunks.join(""));
|
|
218934
219071
|
if (code === 0 && existsSync14(outAbs))
|
|
218935
|
-
return done();
|
|
219072
|
+
return done({ output, exitCode: code });
|
|
219073
|
+
const tail = demangle(chunks.slice(-80).join("")).trimEnd();
|
|
218936
219074
|
const detail = tail.length > 0 ? `
|
|
218937
|
-
${tail
|
|
219075
|
+
${tail}` : "";
|
|
218938
219076
|
fail(new CliError(code === 0 ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}` : `The renderer exited with code ${code}.${detail}`));
|
|
218939
219077
|
});
|
|
218940
219078
|
});
|
|
@@ -219980,12 +220118,22 @@ from \`main.ts\`.
|
|
|
219980
220118
|
\`\`\`
|
|
219981
220119
|
lecodes render # semantic JSON of the launched UI (structure, text, names, values)
|
|
219982
220120
|
lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
|
|
220121
|
+
lecodes render --png card.png --clip card # ONE component/region: a node name or x,y,w,h
|
|
219983
220122
|
lecodes render --desktop --png shot.png # NATIVE render: real Filament 3D + UI + 2D composited —
|
|
219984
220123
|
# use this to SEE 3D scenes / procedural geometry (headless
|
|
219985
220124
|
# renders record 3D as JSON only). Windows/Linux; downloads
|
|
219986
220125
|
# the renderer on first use. --frames <n> for slow loads.
|
|
219987
220126
|
lecodes render --script <file> # drive the app through a scenario (format below)
|
|
220127
|
+
lecodes render --desktop --view iso --png iso.png # 3D: inspection camera fitted to the WHOLE
|
|
220128
|
+
# scene (iso|top|front|side|back [--fov N]) — or --camera x,y,z
|
|
220129
|
+
# --look-at x,y,z; the app's camera is untouched. In scripts:
|
|
220130
|
+
# { "view": "iso", "screenshot": "iso.png" } then { "view": "off" }
|
|
220131
|
+
lecodes render --desktop --script <file> # the same scenario on the NATIVE host (3D/Jolt, real
|
|
220132
|
+
# GPU): coordinates only ({ x, y }), expect.log checked,
|
|
220133
|
+
# text/node/screen expects skipped; deterministic 16 ms frames
|
|
219988
220134
|
lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
|
|
220135
|
+
lecodes test --desktop # the file flows on the NATIVE host (3D/Jolt/real GPU; coordinate
|
|
220136
|
+
# flows only — design flows stay headless)
|
|
219989
220137
|
\`\`\`
|
|
219990
220138
|
|
|
219991
220139
|
Iterate with \`--script\` scenarios and keep the useful ones as \`tests/<name>.flow.json\` — they
|
|
@@ -219994,19 +220142,32 @@ are the regression suite. A failing step dumps the failing frame (JSON + PNG) un
|
|
|
219994
220142
|
|
|
219995
220143
|
## Scenario format (tests/*.flow.json and --script files)
|
|
219996
220144
|
|
|
219997
|
-
One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait
|
|
220145
|
+
One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`, and for
|
|
220146
|
+
games: \`frames\` (advance N fixed 1/60 steps, no sleeping), \`drag\`+\`to\` [+\`ms\`], \`hold\`+\`ms\`,
|
|
220147
|
+
\`key\`+\`ms\` / \`keyDown\` / \`keyUp\` (KeyboardEvent.code: "ArrowRight", "KeyW", "Space")), then an
|
|
219998
220148
|
optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
|
|
219999
|
-
\`value: { target, equals }\`
|
|
220000
|
-
\`
|
|
220149
|
+
\`value: { target, equals }\`, \`log\` — a \`console.log\` line contains it), then outputs
|
|
220150
|
+
(\`screenshot\` [+\`clip\`: a node name or \`{ x, y, width, height }\`], \`json\`). Targets resolve by
|
|
220151
|
+
\`name\` → exact visible text → substring → input placeholder; a tap/drag on empty space reaches
|
|
220152
|
+
the 2D scene (sprite \`click\`/\`touchstart\`, \`ev.track()\`). Envelope keys: \`name\`,
|
|
220001
220153
|
\`width\`/\`height\`, \`settle\`, \`fixtures\`, \`steps\`.
|
|
220002
220154
|
|
|
220003
220155
|
\`\`\`json
|
|
220004
220156
|
{ "steps": [
|
|
220005
220157
|
{ "expect": { "screen": "home", "text": "Taps: 0" } },
|
|
220006
|
-
{ "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" }
|
|
220158
|
+
{ "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" },
|
|
220159
|
+
{ "key": "ArrowRight", "ms": 500, "expect": { "log": "hero x=" } },
|
|
220160
|
+
{ "frames": 60, "screenshot": "hud.png", "clip": "hud" }
|
|
220007
220161
|
] }
|
|
220008
220162
|
\`\`\`
|
|
220009
220163
|
|
|
220164
|
+
Game time is deterministic headless: \`setLoop\`, 2D physics/animation and aspect \`update(dt)\`
|
|
220165
|
+
advance in fixed 1/60 steps driven by \`wait\`/\`frames\`/\`drag\`/\`key\` — never by a wall clock —
|
|
220166
|
+
so a flow that passes once passes every time. \`Input.key(code)\` reads the keys a flow holds.
|
|
220167
|
+
Headless draws UI + the 2D scene (sprites, tilemaps, Box2D); 3D is captured as JSON and Jolt does
|
|
220168
|
+
not simulate — use \`--desktop\` for 3D pixels. \`console.log\` + \`expect.log\` is the assertion
|
|
220169
|
+
channel for game state that has no UI.
|
|
220170
|
+
|
|
220010
220171
|
## Make the app testable
|
|
220011
220172
|
|
|
220012
220173
|
- Give interactive elements a \`name\`: \`UIButton(...).style({ name: "login" })\`.
|
|
@@ -220116,7 +220277,7 @@ var run = async (args) => {
|
|
|
220116
220277
|
}
|
|
220117
220278
|
const appCfg = readAppConfigAt(root);
|
|
220118
220279
|
const win = appCfg?.desktop?.window;
|
|
220119
|
-
const { js, resources } = await buildBundle(args);
|
|
220280
|
+
const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
|
|
220120
220281
|
note("Launching LeCodes Desktop… (close the window to return)");
|
|
220121
220282
|
await runDesktopApp(js, resources, {
|
|
220122
220283
|
width: Number(flagStr(args, "width")) || win?.width,
|
|
@@ -220207,23 +220368,29 @@ var DEV_PRELUDE_JS = `
|
|
|
220207
220368
|
} catch (e) {}
|
|
220208
220369
|
}
|
|
220209
220370
|
|
|
220371
|
+
// An Error carries its stack (JSON.stringify would send a useless "{}"); the dev server maps the
|
|
220372
|
+
// frames back to project positions before printing.
|
|
220373
|
+
function describe(a) {
|
|
220374
|
+
if (typeof a === "string") return a
|
|
220375
|
+
if (a && typeof a.stack === "string" && typeof a.message === "string") {
|
|
220376
|
+
return a.stack ? String(a) + "\\n" + a.stack : String(a)
|
|
220377
|
+
}
|
|
220378
|
+
try { return JSON.stringify(a) } catch (e) { return String(a) }
|
|
220379
|
+
}
|
|
220380
|
+
|
|
220210
220381
|
// Mirror console output to the dev terminal ('silent' guards recursion through wrapped calls).
|
|
220211
220382
|
var silent = false
|
|
220212
220383
|
;["log", "warn", "error"].forEach(function (level) {
|
|
220213
220384
|
var orig = console[level]
|
|
220214
220385
|
console[level] = function () {
|
|
220215
220386
|
var parts = []
|
|
220216
|
-
for (var i = 0; i < arguments.length; i++)
|
|
220217
|
-
var a = arguments[i]
|
|
220218
|
-
if (typeof a === "string") { parts.push(a); continue }
|
|
220219
|
-
try { parts.push(JSON.stringify(a)) } catch (e) { parts.push(String(a)) }
|
|
220220
|
-
}
|
|
220387
|
+
for (var i = 0; i < arguments.length; i++) parts.push(describe(arguments[i]))
|
|
220221
220388
|
try { orig.apply(console, arguments) } catch (e) {}
|
|
220222
220389
|
if (!silent) { silent = true; try { post({ type: "log", level: level, text: parts.join(" ") }) } finally { silent = false } }
|
|
220223
220390
|
}
|
|
220224
220391
|
})
|
|
220225
220392
|
|
|
220226
|
-
function report(err) { post({ type: "error", text:
|
|
220393
|
+
function report(err) { post({ type: "error", text: describe(err) }) }
|
|
220227
220394
|
|
|
220228
220395
|
function load(v) {
|
|
220229
220396
|
if (busy) { queued = v; return }
|
|
@@ -220501,6 +220668,14 @@ var startDevServer = async (options) => {
|
|
|
220501
220668
|
for (const client of clients)
|
|
220502
220669
|
client.send(text);
|
|
220503
220670
|
};
|
|
220671
|
+
let demangleFor = null;
|
|
220672
|
+
const demangle = (text) => {
|
|
220673
|
+
if (bundleJs === null)
|
|
220674
|
+
return text;
|
|
220675
|
+
if (demangleFor?.js !== bundleJs)
|
|
220676
|
+
demangleFor = { js: bundleJs, map: makeStackDemangler(bundleJs) };
|
|
220677
|
+
return demangleFor.map ? demangleFor.map(text) : text;
|
|
220678
|
+
};
|
|
220504
220679
|
const printDeviceMessage = (raw) => {
|
|
220505
220680
|
let m = {};
|
|
220506
220681
|
try {
|
|
@@ -220509,9 +220684,9 @@ var startDevServer = async (options) => {
|
|
|
220509
220684
|
return;
|
|
220510
220685
|
}
|
|
220511
220686
|
if (m.type === "log")
|
|
220512
|
-
log2(`[device:${m.level ?? "log"}] ${m.text ?? ""}`);
|
|
220687
|
+
log2(`[device:${m.level ?? "log"}] ${demangle(m.text ?? "")}`);
|
|
220513
220688
|
else if (m.type === "error")
|
|
220514
|
-
log2(`[device:error] ${m.text ?? ""}`);
|
|
220689
|
+
log2(`[device:error] ${demangle(m.text ?? "")}`);
|
|
220515
220690
|
};
|
|
220516
220691
|
const transportOf = (url) => url.searchParams.get("t") === "poll" ? "poll" : "ws";
|
|
220517
220692
|
const handle = async (req, res) => {
|
|
@@ -221403,7 +221578,7 @@ var login = async (args) => {
|
|
|
221403
221578
|
|
|
221404
221579
|
// src/commands/render.ts
|
|
221405
221580
|
import { mkdirSync as mkdirSync18, readFileSync as readFileSync22, writeFileSync as writeFileSync19 } from "node:fs";
|
|
221406
|
-
import { dirname as dirname15, resolve as
|
|
221581
|
+
import { dirname as dirname15, resolve as resolve12 } from "node:path";
|
|
221407
221582
|
|
|
221408
221583
|
// src/compile/headlessBundle.ts
|
|
221409
221584
|
init_api();
|
|
@@ -221484,6 +221659,240 @@ var compileHeadlessBundle = async (args) => {
|
|
|
221484
221659
|
|
|
221485
221660
|
// src/commands/render.ts
|
|
221486
221661
|
init_manifest();
|
|
221662
|
+
|
|
221663
|
+
// src/desktopScript.ts
|
|
221664
|
+
init_util();
|
|
221665
|
+
import { resolve as resolve11 } from "node:path";
|
|
221666
|
+
var isPoint = (v) => typeof v === "object" && v !== null && typeof v.x === "number" && typeof v.y === "number";
|
|
221667
|
+
var FRAME_MS = 16;
|
|
221668
|
+
var ACTIONS = ["tap", "type", "fill", "scroll", "wait", "frames", "drag", "hold", "key", "keyDown", "keyUp", "camera", "view"];
|
|
221669
|
+
var SEMANTIC_EXPECT = ["text", "notText", "node", "notNode", "screen", "value"];
|
|
221670
|
+
var VIEW_NAMES = ["iso", "top", "front", "side", "back"];
|
|
221671
|
+
var isVec3 = (v) => Array.isArray(v) && v.length === 3 && v.every((n2) => typeof n2 === "number");
|
|
221672
|
+
var pt = (p2) => `(${p2.x},${p2.y})`;
|
|
221673
|
+
var translateScenario = (rawSteps, opts) => {
|
|
221674
|
+
if (!Array.isArray(rawSteps))
|
|
221675
|
+
throw new CliError(`"steps" must be an array of step objects.`);
|
|
221676
|
+
const lines = [];
|
|
221677
|
+
const steps = [];
|
|
221678
|
+
let budgetMs = 0;
|
|
221679
|
+
const emit = (line) => {
|
|
221680
|
+
lines.push(line);
|
|
221681
|
+
return lines.length;
|
|
221682
|
+
};
|
|
221683
|
+
emit(`# generated by lecodes render --desktop --script`);
|
|
221684
|
+
emit(`frames ${Math.max(0, opts.headroomFrames)}`);
|
|
221685
|
+
const coord = (at, key, v) => {
|
|
221686
|
+
if (isPoint(v))
|
|
221687
|
+
return v;
|
|
221688
|
+
if (typeof v === "string") {
|
|
221689
|
+
throw new CliError(`${at}: "${key}": "${v}" — the native host can't resolve names; give --desktop scripts { "x", "y" } coordinates (logical px), or run the scenario headless (without --desktop).`);
|
|
221690
|
+
}
|
|
221691
|
+
throw new CliError(`${at}: "${key}" must be { "x", "y" }.`);
|
|
221692
|
+
};
|
|
221693
|
+
const num = (at, key, v, def) => {
|
|
221694
|
+
if (v === undefined && def !== undefined)
|
|
221695
|
+
return def;
|
|
221696
|
+
if (typeof v !== "number")
|
|
221697
|
+
throw new CliError(`${at}: "${key}" must be a number.`);
|
|
221698
|
+
return v;
|
|
221699
|
+
};
|
|
221700
|
+
let inspecting = false;
|
|
221701
|
+
rawSteps.forEach((step, i2) => {
|
|
221702
|
+
const at = `steps[${i2}]`;
|
|
221703
|
+
if (typeof step !== "object" || step === null || Array.isArray(step))
|
|
221704
|
+
throw new CliError(`${at}: must be an object.`);
|
|
221705
|
+
const s2 = step;
|
|
221706
|
+
const actions = ACTIONS.filter((k) => s2[k] !== undefined);
|
|
221707
|
+
if (actions.length > 1)
|
|
221708
|
+
throw new CliError(`${at}: one action per step (found ${actions.join(" + ")}).`);
|
|
221709
|
+
const plan = { index: i2, desc: "", lines: [], skippedExpect: [], skippedOutputs: [] };
|
|
221710
|
+
const parts = [];
|
|
221711
|
+
const action = actions[0];
|
|
221712
|
+
if (action === "type" || action === "fill" || action === "scroll") {
|
|
221713
|
+
throw new CliError(`${at}: "${action}" isn't available with --desktop (the native host has no semantic tree / soft keyboard) — run this scenario headless, or drive the gesture with "drag" coordinates.`);
|
|
221714
|
+
}
|
|
221715
|
+
if ((action === "tap" || action === "drag" || action === "hold") && inspecting) {
|
|
221716
|
+
throw new CliError(`${at}: "${action}" while an inspection camera is active — the app's hit-testing uses its own camera, not the one in the shot. Add { "view": "off" } (or { "camera": "off" }) first.`);
|
|
221717
|
+
}
|
|
221718
|
+
if (action === "view") {
|
|
221719
|
+
const v = s2.view;
|
|
221720
|
+
const fov = s2.fov !== undefined ? num(at, "fov", s2.fov) : undefined;
|
|
221721
|
+
if (v === "off") {
|
|
221722
|
+
plan.lines.push(emit("view off"));
|
|
221723
|
+
inspecting = false;
|
|
221724
|
+
parts.push("view off");
|
|
221725
|
+
} else if (typeof v === "string" && VIEW_NAMES.includes(v)) {
|
|
221726
|
+
plan.lines.push(emit(`view ${v}${fov !== undefined ? ` ${fov}` : ""}`));
|
|
221727
|
+
inspecting = true;
|
|
221728
|
+
parts.push(`view ${v}${fov !== undefined ? ` (fov ${fov})` : ""}`);
|
|
221729
|
+
} else
|
|
221730
|
+
throw new CliError(`${at}: "view" wants one of ${VIEW_NAMES.join(", ")} or "off".`);
|
|
221731
|
+
budgetMs += 2 * FRAME_MS;
|
|
221732
|
+
} else if (action === "camera") {
|
|
221733
|
+
const cam = s2.camera;
|
|
221734
|
+
if (cam === "off") {
|
|
221735
|
+
plan.lines.push(emit("camera off"));
|
|
221736
|
+
inspecting = false;
|
|
221737
|
+
parts.push("camera off");
|
|
221738
|
+
} else if (typeof cam === "object" && cam !== null && isVec3(cam.at) && isVec3(cam.lookAt)) {
|
|
221739
|
+
const { at: eye, lookAt, fov } = cam;
|
|
221740
|
+
const fovN = fov !== undefined ? num(at, "camera.fov", fov) : undefined;
|
|
221741
|
+
plan.lines.push(emit(`camera ${eye.join(" ")} ${lookAt.join(" ")}${fovN !== undefined ? ` ${fovN}` : ""}`));
|
|
221742
|
+
inspecting = true;
|
|
221743
|
+
parts.push(`camera at (${eye.join(",")}) → (${lookAt.join(",")})`);
|
|
221744
|
+
} else
|
|
221745
|
+
throw new CliError(`${at}: "camera" wants { "at": [x,y,z], "lookAt": [x,y,z], "fov"? } or "off".`);
|
|
221746
|
+
budgetMs += 2 * FRAME_MS;
|
|
221747
|
+
}
|
|
221748
|
+
if (s2.fov !== undefined && action !== "view")
|
|
221749
|
+
throw new CliError(`${at}: "fov" only goes with "view" (inside "camera" use camera.fov).`);
|
|
221750
|
+
if (action === "tap") {
|
|
221751
|
+
const p2 = coord(at, "tap", s2.tap);
|
|
221752
|
+
plan.lines.push(emit(`tap ${p2.x} ${p2.y}`));
|
|
221753
|
+
budgetMs += 4 * FRAME_MS;
|
|
221754
|
+
parts.push(`tap ${pt(p2)}`);
|
|
221755
|
+
} else if (action === "drag") {
|
|
221756
|
+
const a2 = coord(at, "drag", s2.drag), b = coord(at, "to", s2.to);
|
|
221757
|
+
const ms = num(at, "ms", s2.ms, 300);
|
|
221758
|
+
plan.lines.push(emit(`drag ${a2.x} ${a2.y} ${b.x} ${b.y} ${Math.round(ms)}`));
|
|
221759
|
+
budgetMs += ms + 4 * FRAME_MS;
|
|
221760
|
+
parts.push(`drag ${pt(a2)} → ${pt(b)} over ${ms}ms`);
|
|
221761
|
+
} else if (action === "hold") {
|
|
221762
|
+
const p2 = coord(at, "hold", s2.hold);
|
|
221763
|
+
const ms = num(at, "ms", s2.ms);
|
|
221764
|
+
plan.lines.push(emit(`hold ${p2.x} ${p2.y} ${Math.round(ms)}`));
|
|
221765
|
+
budgetMs += ms + 4 * FRAME_MS;
|
|
221766
|
+
parts.push(`hold ${pt(p2)} ${ms}ms`);
|
|
221767
|
+
} else if (action === "key") {
|
|
221768
|
+
if (typeof s2.key !== "string")
|
|
221769
|
+
throw new CliError(`${at}: "key" must be a KeyboardEvent.code string.`);
|
|
221770
|
+
const ms = num(at, "ms", s2.ms);
|
|
221771
|
+
plan.lines.push(emit(`key ${s2.key} ${Math.round(ms)}`));
|
|
221772
|
+
budgetMs += ms + 2 * FRAME_MS;
|
|
221773
|
+
parts.push(`key ${s2.key} ${ms}ms`);
|
|
221774
|
+
} else if (action === "keyDown" || action === "keyUp") {
|
|
221775
|
+
const code = s2[action];
|
|
221776
|
+
if (typeof code !== "string")
|
|
221777
|
+
throw new CliError(`${at}: "${action}" must be a code string.`);
|
|
221778
|
+
plan.lines.push(emit(`${action.toLowerCase()} ${code}`));
|
|
221779
|
+
budgetMs += 2 * FRAME_MS;
|
|
221780
|
+
parts.push(`${action} ${code}`);
|
|
221781
|
+
} else if (action === "wait") {
|
|
221782
|
+
const ms = num(at, "wait", s2.wait);
|
|
221783
|
+
plan.lines.push(emit(`wait ${Math.round(ms)}`));
|
|
221784
|
+
budgetMs += ms + FRAME_MS;
|
|
221785
|
+
parts.push(`wait ${ms}ms`);
|
|
221786
|
+
} else if (action === "frames") {
|
|
221787
|
+
const n2 = num(at, "frames", s2.frames);
|
|
221788
|
+
plan.lines.push(emit(`frames ${Math.round(n2)}`));
|
|
221789
|
+
budgetMs += n2 * FRAME_MS + FRAME_MS;
|
|
221790
|
+
parts.push(`advance ${n2} frames`);
|
|
221791
|
+
}
|
|
221792
|
+
if (s2.expect !== undefined) {
|
|
221793
|
+
if (typeof s2.expect !== "object" || s2.expect === null)
|
|
221794
|
+
throw new CliError(`${at}: "expect" must be an object.`);
|
|
221795
|
+
const e2 = s2.expect;
|
|
221796
|
+
const bits = [];
|
|
221797
|
+
for (const k of Object.keys(e2)) {
|
|
221798
|
+
if (k === "log") {
|
|
221799
|
+
if (typeof e2.log !== "string")
|
|
221800
|
+
throw new CliError(`${at}: expect.log must be a string.`);
|
|
221801
|
+
plan.expectLog = e2.log;
|
|
221802
|
+
bits.push(`log~"${e2.log}"`);
|
|
221803
|
+
} else if (SEMANTIC_EXPECT.includes(k)) {
|
|
221804
|
+
plan.skippedExpect.push(k);
|
|
221805
|
+
} else {
|
|
221806
|
+
throw new CliError(`${at}: unknown expect key "${k}".`);
|
|
221807
|
+
}
|
|
221808
|
+
}
|
|
221809
|
+
if (bits.length > 0)
|
|
221810
|
+
parts.push(`expect ${bits.join(" · ")}`);
|
|
221811
|
+
}
|
|
221812
|
+
if (s2.clip !== undefined)
|
|
221813
|
+
throw new CliError(`${at}: "clip" isn't available with --desktop (the native host dumps the whole window).`);
|
|
221814
|
+
if (s2.screenshot !== undefined) {
|
|
221815
|
+
if (typeof s2.screenshot !== "string")
|
|
221816
|
+
throw new CliError(`${at}: "screenshot" must be a path.`);
|
|
221817
|
+
plan.screenshot = resolve11(opts.outDir, s2.screenshot);
|
|
221818
|
+
plan.lines.push(emit(`frames 2`));
|
|
221819
|
+
plan.lines.push(emit(`shot ${plan.screenshot}`));
|
|
221820
|
+
budgetMs += 3 * FRAME_MS;
|
|
221821
|
+
parts.push(`→ ${s2.screenshot}`);
|
|
221822
|
+
}
|
|
221823
|
+
if (s2.json !== undefined)
|
|
221824
|
+
plan.skippedOutputs.push("json");
|
|
221825
|
+
if (plan.lines.length === 0) {
|
|
221826
|
+
plan.lines.push(emit(`frames 1`));
|
|
221827
|
+
budgetMs += FRAME_MS;
|
|
221828
|
+
}
|
|
221829
|
+
plan.desc = typeof s2.name === "string" ? s2.name : parts.join(", ") || "(empty step)";
|
|
221830
|
+
steps.push(plan);
|
|
221831
|
+
});
|
|
221832
|
+
const frameCap = Math.max(0, opts.headroomFrames) + Math.ceil(budgetMs / FRAME_MS) + 600;
|
|
221833
|
+
return { text: lines.join(`
|
|
221834
|
+
`) + `
|
|
221835
|
+
`, steps, frameCap };
|
|
221836
|
+
};
|
|
221837
|
+
var evaluateDesktopRun = (plan, output) => {
|
|
221838
|
+
const lines = output.split(/\r?\n/);
|
|
221839
|
+
const doneAt = new Map;
|
|
221840
|
+
const consoleLines = [];
|
|
221841
|
+
let loaded = false, ended = false;
|
|
221842
|
+
for (const line of lines) {
|
|
221843
|
+
const m = /^\[script\] #(\d+) (done|error) (\S+)(?:: (.*))?$/.exec(line);
|
|
221844
|
+
if (m) {
|
|
221845
|
+
doneAt.set(Number(m[1]), { at: consoleLines.length, error: m[2] === "error" ? `${m[3]}: ${m[4] ?? "failed"}` : undefined });
|
|
221846
|
+
continue;
|
|
221847
|
+
}
|
|
221848
|
+
if (line.startsWith("[script] loaded")) {
|
|
221849
|
+
loaded = true;
|
|
221850
|
+
continue;
|
|
221851
|
+
}
|
|
221852
|
+
if (line === "[script] end") {
|
|
221853
|
+
ended = true;
|
|
221854
|
+
continue;
|
|
221855
|
+
}
|
|
221856
|
+
if (line.startsWith("[script]"))
|
|
221857
|
+
continue;
|
|
221858
|
+
consoleLines.push(line);
|
|
221859
|
+
}
|
|
221860
|
+
const steps = [];
|
|
221861
|
+
let ok = true;
|
|
221862
|
+
for (const st of plan.steps) {
|
|
221863
|
+
const skipped = [
|
|
221864
|
+
...st.skippedExpect.map((k) => `expect.${k} (no semantic tree on --desktop)`),
|
|
221865
|
+
...st.skippedOutputs.map((k) => `${k} output (no semantic tree on --desktop)`)
|
|
221866
|
+
];
|
|
221867
|
+
let error;
|
|
221868
|
+
let cutoff = consoleLines.length;
|
|
221869
|
+
for (const ln of st.lines) {
|
|
221870
|
+
const d = doneAt.get(ln);
|
|
221871
|
+
if (!d) {
|
|
221872
|
+
error = error ?? "the host never completed this step (crashed, closed, or hit the frame cap before it)";
|
|
221873
|
+
break;
|
|
221874
|
+
}
|
|
221875
|
+
if (d.error) {
|
|
221876
|
+
error = error ?? d.error;
|
|
221877
|
+
break;
|
|
221878
|
+
}
|
|
221879
|
+
cutoff = d.at;
|
|
221880
|
+
}
|
|
221881
|
+
if (!error && st.expectLog !== undefined) {
|
|
221882
|
+
const seen = consoleLines.slice(0, cutoff);
|
|
221883
|
+
if (!seen.some((l2) => l2.includes(st.expectLog))) {
|
|
221884
|
+
const tail = seen.filter((l2) => l2.startsWith("[DEBUG]") || l2.startsWith("[INFO]") || l2.startsWith("[WARN]") || l2.startsWith("[ERROR]")).slice(-3);
|
|
221885
|
+
error = `expected a console line containing "${st.expectLog}" — ${tail.length === 0 ? "the app logged nothing" : `last lines: ${tail.map((l2) => `"${l2}"`).join(", ")}`}`;
|
|
221886
|
+
}
|
|
221887
|
+
}
|
|
221888
|
+
if (error)
|
|
221889
|
+
ok = false;
|
|
221890
|
+
steps.push({ desc: st.desc, ok: !error, error, skipped });
|
|
221891
|
+
}
|
|
221892
|
+
return { loaded, ended, ok: ok && loaded, steps };
|
|
221893
|
+
};
|
|
221894
|
+
|
|
221895
|
+
// src/commands/render.ts
|
|
221487
221896
|
init_util();
|
|
221488
221897
|
var DEVICE_SAFE_AREA = {
|
|
221489
221898
|
none: { insets: [0, 0, 0, 0], bars: [] },
|
|
@@ -221514,7 +221923,7 @@ var requireRenderer = async () => {
|
|
|
221514
221923
|
};
|
|
221515
221924
|
var fileScenarioIO = (dir) => ({
|
|
221516
221925
|
write: (relPath, data) => {
|
|
221517
|
-
const abs =
|
|
221926
|
+
const abs = resolve12(dir, relPath);
|
|
221518
221927
|
mkdirSync18(dirname15(abs), { recursive: true });
|
|
221519
221928
|
writeFileSync19(abs, data);
|
|
221520
221929
|
}
|
|
@@ -221524,7 +221933,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
221524
221933
|
return;
|
|
221525
221934
|
let map = value;
|
|
221526
221935
|
if (typeof value === "string") {
|
|
221527
|
-
const abs =
|
|
221936
|
+
const abs = resolve12(baseDir, value);
|
|
221528
221937
|
try {
|
|
221529
221938
|
map = JSON.parse(readFileSync22(abs, "utf8"));
|
|
221530
221939
|
} catch (e2) {
|
|
@@ -221539,7 +221948,7 @@ var resolveFixtures = (value, baseDir) => {
|
|
|
221539
221948
|
var readScenarioFile = (path2) => {
|
|
221540
221949
|
let raw;
|
|
221541
221950
|
try {
|
|
221542
|
-
raw = JSON.parse(readFileSync22(
|
|
221951
|
+
raw = JSON.parse(readFileSync22(resolve12(path2), "utf8"));
|
|
221543
221952
|
} catch (e2) {
|
|
221544
221953
|
throw new CliError(`Can't read scenario "${path2}": ${e2 instanceof Error ? e2.message : String(e2)}`);
|
|
221545
221954
|
}
|
|
@@ -221552,6 +221961,20 @@ var readScenarioFile = (path2) => {
|
|
|
221552
221961
|
throw new CliError(`Scenario "${path2}" has no "steps".`);
|
|
221553
221962
|
return file;
|
|
221554
221963
|
};
|
|
221964
|
+
var parseClip = (raw) => {
|
|
221965
|
+
if (raw === undefined)
|
|
221966
|
+
return;
|
|
221967
|
+
const parts = raw.split(",").map((p2) => p2.trim());
|
|
221968
|
+
if (parts.length === 4 && parts.every((p2) => /^-?\d+(\.\d+)?$/.test(p2))) {
|
|
221969
|
+
const [x2, y2, width, height] = parts.map(Number);
|
|
221970
|
+
if (width <= 0 || height <= 0)
|
|
221971
|
+
throw new CliError(`--clip: width and height must be positive (got "${raw}").`);
|
|
221972
|
+
return { x: x2, y: y2, width, height };
|
|
221973
|
+
}
|
|
221974
|
+
if (parts.length === 1 && raw.trim() !== "")
|
|
221975
|
+
return raw.trim();
|
|
221976
|
+
throw new CliError(`--clip: expected a node name or "x,y,width,height" (got "${raw}").`);
|
|
221977
|
+
};
|
|
221555
221978
|
var renderDesktop = async (args) => {
|
|
221556
221979
|
let root;
|
|
221557
221980
|
try {
|
|
@@ -221560,22 +221983,101 @@ var renderDesktop = async (args) => {
|
|
|
221560
221983
|
root = process.cwd();
|
|
221561
221984
|
}
|
|
221562
221985
|
const win = readAppConfigAt(root)?.desktop?.window;
|
|
221563
|
-
const
|
|
221564
|
-
const
|
|
221986
|
+
const scriptFlag = flagStr(args, "script");
|
|
221987
|
+
const scenario = scriptFlag !== undefined ? readScenarioFile(scriptFlag) : null;
|
|
221988
|
+
const width = Number(flagStr(args, "width")) || scenario?.width || win?.width || 390;
|
|
221989
|
+
const height = Number(flagStr(args, "height")) || scenario?.height || win?.height || 844;
|
|
221565
221990
|
const frames = Number(flagStr(args, "frames")) || 60;
|
|
221566
|
-
const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
|
|
221567
221991
|
const pngFlag = flagStr(args, "png") ?? "screenshot.png";
|
|
221568
|
-
const
|
|
221992
|
+
const logs = flagBool(args, "logs");
|
|
221993
|
+
const fixedDtFlag = flagStr(args, "fixed-dt");
|
|
221994
|
+
const fixedDtMs = fixedDtFlag !== undefined ? Number(fixedDtFlag) : scenario ? 16 : 0;
|
|
221995
|
+
if (!Number.isFinite(fixedDtMs) || fixedDtMs < 0 || fixedDtMs > 1000)
|
|
221996
|
+
throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
|
|
221997
|
+
const { camera, view } = resolveInspectionCamera(args);
|
|
221998
|
+
if ((camera || view) && scenario)
|
|
221999
|
+
throw new CliError('--camera/--view apply to a plain render; in a --script run use a { "view": "iso" } or { "camera": { "at", "lookAt" } } step before the screenshot.');
|
|
222000
|
+
const { js, resources } = await buildBundle(args, undefined, { localAssets: true });
|
|
222001
|
+
mkdirSync18(dirname15(resolve12(pngFlag)), { recursive: true });
|
|
222002
|
+
if (scenario) {
|
|
222003
|
+
const outDir = flagStr(args, "out-dir") ?? ".";
|
|
222004
|
+
const plan = translateScenario(scenario.steps, { outDir, headroomFrames: frames });
|
|
222005
|
+
for (const st of plan.steps)
|
|
222006
|
+
if (st.screenshot)
|
|
222007
|
+
mkdirSync18(dirname15(st.screenshot), { recursive: true });
|
|
222008
|
+
const timeoutMs2 = Number(flagStr(args, "timeout")) || 60000 + plan.steps.length * 5000;
|
|
222009
|
+
note(`Running ${plan.steps.length} step${plan.steps.length === 1 ? "" : "s"} (native)…`);
|
|
222010
|
+
const { output } = await runDesktopRender(js, resources, {
|
|
222011
|
+
width,
|
|
222012
|
+
height,
|
|
222013
|
+
frames: plan.frameCap,
|
|
222014
|
+
outPng: pngFlag,
|
|
222015
|
+
logs,
|
|
222016
|
+
timeoutMs: timeoutMs2,
|
|
222017
|
+
script: plan.text,
|
|
222018
|
+
fixedDtMs
|
|
222019
|
+
});
|
|
222020
|
+
const result = evaluateDesktopRun(plan, output);
|
|
222021
|
+
if (!result.loaded) {
|
|
222022
|
+
throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT) — it ran the frames and ignored the steps. Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
|
|
222023
|
+
}
|
|
222024
|
+
for (const st of result.steps) {
|
|
222025
|
+
logErr(st.ok ? `${c.green("✓")} ${st.desc}` : `${c.red("✗")} ${st.desc}
|
|
222026
|
+
${c.red(st.error ?? "failed")}`);
|
|
222027
|
+
for (const sk of st.skipped)
|
|
222028
|
+
logErr(c.dim(` – skipped: ${sk}`));
|
|
222029
|
+
}
|
|
222030
|
+
const skippedCount = result.steps.reduce((n2, s2) => n2 + s2.skipped.length, 0);
|
|
222031
|
+
if (skippedCount > 0)
|
|
222032
|
+
logErr(c.dim(` ${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped — the native host has no UI tree; run the same scenario headless to verify them.`));
|
|
222033
|
+
if (!result.ended)
|
|
222034
|
+
warnErr("The host stopped before the script finished (frame cap / crash) — later steps are reported as incomplete.");
|
|
222035
|
+
logErr(result.ok ? c.green(`✓ ${result.steps.length}/${result.steps.length} steps passed (native)`) : c.red(`✗ ${result.steps.filter((s2) => !s2.ok).length} of ${result.steps.length} steps failed (native)`));
|
|
222036
|
+
note(`Wrote ${pngFlag} (${width}x${height}, native render, final frame)`);
|
|
222037
|
+
process.exit(result.ok ? 0 : 1);
|
|
222038
|
+
}
|
|
222039
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || 60000;
|
|
221569
222040
|
note("Rendering (native)…");
|
|
221570
222041
|
await runDesktopRender(js, resources, {
|
|
221571
222042
|
width,
|
|
221572
222043
|
height,
|
|
221573
222044
|
frames,
|
|
221574
222045
|
outPng: pngFlag,
|
|
221575
|
-
logs
|
|
221576
|
-
timeoutMs
|
|
222046
|
+
logs,
|
|
222047
|
+
timeoutMs,
|
|
222048
|
+
fixedDtMs,
|
|
222049
|
+
camera,
|
|
222050
|
+
view
|
|
221577
222051
|
});
|
|
221578
|
-
note(`Wrote ${pngFlag} (${width}x${height}, native render)`);
|
|
222052
|
+
note(`Wrote ${pngFlag} (${width}x${height}, native render${view ? `, ${view.split(";")[0]} view` : camera ? ", custom camera" : ""})`);
|
|
222053
|
+
};
|
|
222054
|
+
var VIEW_NAMES2 = ["iso", "top", "front", "side", "back"];
|
|
222055
|
+
var parseVec3Flag = (name, raw) => {
|
|
222056
|
+
const parts = raw.split(",").map((p2) => p2.trim());
|
|
222057
|
+
if (parts.length !== 3 || parts.some((p2) => !/^-?\d+(\.\d+)?$/.test(p2)))
|
|
222058
|
+
throw new CliError(`--${name} wants "x,y,z" (got "${raw}").`);
|
|
222059
|
+
return parts.join(",");
|
|
222060
|
+
};
|
|
222061
|
+
var resolveInspectionCamera = (args) => {
|
|
222062
|
+
const cam = flagStr(args, "camera"), look = flagStr(args, "look-at"), view = flagStr(args, "view"), fov = flagStr(args, "fov");
|
|
222063
|
+
if (fov !== undefined && !(Number(fov) > 0 && Number(fov) < 180))
|
|
222064
|
+
throw new CliError(`--fov wants degrees in (0, 180), got "${fov}".`);
|
|
222065
|
+
const fovSuffix = fov !== undefined ? `;${Number(fov)}` : "";
|
|
222066
|
+
if (cam !== undefined || look !== undefined) {
|
|
222067
|
+
if (view !== undefined)
|
|
222068
|
+
throw new CliError("Use either --camera/--look-at or --view, not both.");
|
|
222069
|
+
if (cam === undefined || look === undefined)
|
|
222070
|
+
throw new CliError("--camera and --look-at go together: --camera x,y,z --look-at x,y,z");
|
|
222071
|
+
return { camera: `${parseVec3Flag("camera", cam)};${parseVec3Flag("look-at", look)}${fovSuffix}` };
|
|
222072
|
+
}
|
|
222073
|
+
if (view !== undefined) {
|
|
222074
|
+
if (!VIEW_NAMES2.includes(view))
|
|
222075
|
+
throw new CliError(`--view wants one of ${VIEW_NAMES2.join(", ")} (got "${view}").`);
|
|
222076
|
+
return { view: `${view}${fovSuffix}` };
|
|
222077
|
+
}
|
|
222078
|
+
if (fov !== undefined)
|
|
222079
|
+
throw new CliError("--fov goes with --camera/--look-at or --view.");
|
|
222080
|
+
return {};
|
|
221579
222081
|
};
|
|
221580
222082
|
var render = async (args) => {
|
|
221581
222083
|
if (flagBool(args, "desktop"))
|
|
@@ -221597,7 +222099,7 @@ var render = async (args) => {
|
|
|
221597
222099
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
221598
222100
|
const timeStr = flagStr(args, "time");
|
|
221599
222101
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
221600
|
-
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(
|
|
222102
|
+
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname15(resolve12(scriptFlag)) : ".");
|
|
221601
222103
|
const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
|
|
221602
222104
|
const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
|
|
221603
222105
|
if (fixtures) {
|
|
@@ -221636,7 +222138,7 @@ var render = async (args) => {
|
|
|
221636
222138
|
${c.red(st.error ?? "failed")}`);
|
|
221637
222139
|
}
|
|
221638
222140
|
if (!result2.ok && result2.failureArtifacts.length > 0) {
|
|
221639
|
-
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) =>
|
|
222141
|
+
logErr(c.dim(` failing frame: ${result2.failureArtifacts.map((p2) => resolve12(outDir, p2)).join(", ")}`));
|
|
221640
222142
|
}
|
|
221641
222143
|
logErr(result2.ok ? c.green(`✓ ${result2.steps.length}/${steps.length} steps passed`) : c.red(`✗ failed at step ${result2.steps.length} of ${steps.length}`));
|
|
221642
222144
|
return done((cb) => cb(), result2.ok ? 0 : 1);
|
|
@@ -221647,19 +222149,21 @@ var render = async (args) => {
|
|
|
221647
222149
|
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
221648
222150
|
if (pngFlag !== undefined) {
|
|
221649
222151
|
note("Rendering PNG…");
|
|
221650
|
-
const
|
|
222152
|
+
const clip = parseClip(flagStr(args, "clip"));
|
|
222153
|
+
const { png: png2, width: outW, height: outH, warnings: pngWarnings } = await renderer.renderToPng(js, {
|
|
221651
222154
|
width,
|
|
221652
222155
|
height,
|
|
221653
222156
|
scale: Number(flagStr(args, "scale")) || 2,
|
|
221654
|
-
...timing
|
|
222157
|
+
...timing,
|
|
222158
|
+
clip
|
|
221655
222159
|
});
|
|
221656
222160
|
for (const w of [...warnings, ...pngWarnings])
|
|
221657
222161
|
warnErr(w);
|
|
221658
222162
|
if (isNullSink(pngFlag)) {
|
|
221659
222163
|
note(`Discarded PNG (${pngFlag})`);
|
|
221660
222164
|
} else {
|
|
221661
|
-
writeFileSync19(
|
|
221662
|
-
note(`Wrote ${pngFlag} (${png2.length} bytes, ${
|
|
222165
|
+
writeFileSync19(resolve12(pngFlag), png2);
|
|
222166
|
+
note(`Wrote ${pngFlag} (${png2.length} bytes, ${outW}x${outH}${clip !== undefined ? ", clipped" : ""})`);
|
|
221663
222167
|
}
|
|
221664
222168
|
return done((cb) => cb());
|
|
221665
222169
|
}
|
|
@@ -221673,7 +222177,7 @@ var render = async (args) => {
|
|
|
221673
222177
|
return done((cb) => cb());
|
|
221674
222178
|
}
|
|
221675
222179
|
if (out) {
|
|
221676
|
-
writeFileSync19(
|
|
222180
|
+
writeFileSync19(resolve12(out), result.text);
|
|
221677
222181
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
221678
222182
|
return done((cb) => cb());
|
|
221679
222183
|
}
|
|
@@ -222034,7 +222538,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
222034
222538
|
// src/commands/scene.ts
|
|
222035
222539
|
init_manifest();
|
|
222036
222540
|
import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync12, statSync as statSync8, writeFileSync as writeFileSync22 } from "node:fs";
|
|
222037
|
-
import { basename as basename14, dirname as dirname17, join as join27, resolve as
|
|
222541
|
+
import { basename as basename14, dirname as dirname17, join as join27, resolve as resolve13 } from "node:path";
|
|
222038
222542
|
|
|
222039
222543
|
// src/compile/sceneCompile.ts
|
|
222040
222544
|
init_localFiles();
|
|
@@ -222107,7 +222611,7 @@ var getContext2 = (args) => {
|
|
|
222107
222611
|
manifest = null;
|
|
222108
222612
|
const pathArg = args._[0];
|
|
222109
222613
|
if (pathArg) {
|
|
222110
|
-
const abs =
|
|
222614
|
+
const abs = resolve13(process.cwd(), pathArg);
|
|
222111
222615
|
if (existsSync27(abs) && statSync8(abs).isDirectory())
|
|
222112
222616
|
root = abs;
|
|
222113
222617
|
else if (abs.endsWith(".scene.ts") && existsSync27(abs))
|
|
@@ -222235,8 +222739,9 @@ var status2 = async (_args) => {
|
|
|
222235
222739
|
};
|
|
222236
222740
|
|
|
222237
222741
|
// src/commands/test.ts
|
|
222238
|
-
import { existsSync as existsSync28, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
|
|
222239
|
-
import { dirname as dirname18, resolve as
|
|
222742
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync20, readFileSync as readFileSync26, readdirSync as readdirSync13 } from "node:fs";
|
|
222743
|
+
import { dirname as dirname18, resolve as resolve14 } from "node:path";
|
|
222744
|
+
init_manifest();
|
|
222240
222745
|
init_util();
|
|
222241
222746
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
222242
222747
|
const edges = meta.edges ?? [];
|
|
@@ -222321,17 +222826,39 @@ var readFlowFile = (abs, displayName) => {
|
|
|
222321
222826
|
};
|
|
222322
222827
|
};
|
|
222323
222828
|
var test = async (args) => {
|
|
222324
|
-
const
|
|
222325
|
-
|
|
222829
|
+
const desktop2 = flagBool(args, "desktop");
|
|
222830
|
+
if (desktop2 && flagBool(args, "flows"))
|
|
222831
|
+
throw new CliError("--flows needs the headless renderer (design flows tap by name) — drop --desktop.");
|
|
222832
|
+
const native = desktop2 ? await buildBundle(args, undefined, { localAssets: true }) : null;
|
|
222833
|
+
const headless = desktop2 ? null : await compileHeadlessBundle(args);
|
|
222834
|
+
const warnings = headless?.warnings ?? [];
|
|
222835
|
+
const useLocalAssets = headless?.useLocalAssets ?? false;
|
|
222836
|
+
const js = headless?.js ?? "";
|
|
222837
|
+
let root;
|
|
222838
|
+
if (headless)
|
|
222839
|
+
root = headless.root;
|
|
222840
|
+
else {
|
|
222841
|
+
try {
|
|
222842
|
+
root = findProjectRoot(process.cwd());
|
|
222843
|
+
} catch {
|
|
222844
|
+
root = process.cwd();
|
|
222845
|
+
}
|
|
222846
|
+
}
|
|
222847
|
+
const renderer = desktop2 ? null : await requireRenderer();
|
|
222848
|
+
const nativeFrames = Number(flagStr(args, "frames")) || 60;
|
|
222849
|
+
const fixedDtFlag = flagStr(args, "fixed-dt");
|
|
222850
|
+
const nativeFixedDt = fixedDtFlag !== undefined ? Number(fixedDtFlag) : 16;
|
|
222851
|
+
if (!Number.isFinite(nativeFixedDt) || nativeFixedDt < 0 || nativeFixedDt > 1000)
|
|
222852
|
+
throw new CliError(`--fixed-dt wants a number of ms (0 = wall clock, max 1000), got "${fixedDtFlag}".`);
|
|
222326
222853
|
const projectViewport = desktopDefaultViewport(root);
|
|
222327
|
-
const testsDir =
|
|
222328
|
-
const designMetaPath =
|
|
222854
|
+
const testsDir = resolve14(root, flagStr(args, "dir") ?? "tests");
|
|
222855
|
+
const designMetaPath = resolve14(root, flagStr(args, "design-dir") ?? "design", "meta.json");
|
|
222329
222856
|
const asJson = flagBool(args, "json");
|
|
222330
222857
|
const out = asJson ? logErr : log;
|
|
222331
222858
|
const cases = [];
|
|
222332
222859
|
if (args._.length > 0) {
|
|
222333
222860
|
for (const given of args._) {
|
|
222334
|
-
const candidates = [
|
|
222861
|
+
const candidates = [resolve14(given), resolve14(testsDir, given), resolve14(testsDir, `${given}.flow.json`)];
|
|
222335
222862
|
const abs = candidates.find((p2) => existsSync28(p2));
|
|
222336
222863
|
if (!abs)
|
|
222337
222864
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
@@ -222339,11 +222866,11 @@ var test = async (args) => {
|
|
|
222339
222866
|
}
|
|
222340
222867
|
} else if (existsSync28(testsDir)) {
|
|
222341
222868
|
for (const f3 of readdirSync13(testsDir).filter((f4) => f4.endsWith(".flow.json")).sort()) {
|
|
222342
|
-
cases.push(readFlowFile(
|
|
222869
|
+
cases.push(readFlowFile(resolve14(testsDir, f3), f3));
|
|
222343
222870
|
}
|
|
222344
222871
|
}
|
|
222345
222872
|
const fixturesFlag = flagStr(args, "fixtures");
|
|
222346
|
-
const defaultPack =
|
|
222873
|
+
const defaultPack = resolve14(testsDir, "fixtures.json");
|
|
222347
222874
|
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync28(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
222348
222875
|
const flowsOnly = flagBool(args, "flows");
|
|
222349
222876
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
@@ -222351,7 +222878,10 @@ var test = async (args) => {
|
|
|
222351
222878
|
cases.length = 0;
|
|
222352
222879
|
let skipped = [];
|
|
222353
222880
|
let entryScreen = null;
|
|
222354
|
-
if (
|
|
222881
|
+
if (desktop2 && wantFlows && existsSync28(designMetaPath)) {
|
|
222882
|
+
note("Design-derived flows skipped with --desktop (they tap by name; run `lecodes test` headless for them).");
|
|
222883
|
+
}
|
|
222884
|
+
if (!desktop2 && (wantFlows || flowsOnly) && existsSync28(designMetaPath)) {
|
|
222355
222885
|
let meta;
|
|
222356
222886
|
try {
|
|
222357
222887
|
meta = JSON.parse(readFileSync26(designMetaPath, "utf8"));
|
|
@@ -222379,18 +222909,60 @@ var test = async (args) => {
|
|
|
222379
222909
|
warnErr(w);
|
|
222380
222910
|
throw new CliError(`No tests found. Write tests/*.flow.json scenarios, or draw activator edges in the design board (design/meta.json).`);
|
|
222381
222911
|
}
|
|
222382
|
-
const timeoutMs = Number(flagStr(args, "timeout")) || 20000 + cases.length * 1e4;
|
|
222912
|
+
const timeoutMs = Number(flagStr(args, "timeout")) || (desktop2 ? 60000 + cases.length * 60000 : 20000 + cases.length * 1e4);
|
|
222383
222913
|
const killer = setTimeout(() => {
|
|
222384
222914
|
warnErr(`Test run timed out after ${timeoutMs}ms (raise --timeout or lower --settle).`);
|
|
222385
222915
|
process.exit(1);
|
|
222386
222916
|
}, timeoutMs);
|
|
222387
222917
|
killer.unref?.();
|
|
222388
222918
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
222389
|
-
const artifactsRoot =
|
|
222919
|
+
const artifactsRoot = resolve14(root, flagStr(args, "artifacts") ?? resolve14(testsDir, ".artifacts"));
|
|
222390
222920
|
const results = [];
|
|
222921
|
+
const runNativeCase = async (kase) => {
|
|
222922
|
+
const caseDir = resolve14(artifactsRoot, slug3(kase.name));
|
|
222923
|
+
const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames });
|
|
222924
|
+
for (const st of plan.steps)
|
|
222925
|
+
if (st.screenshot)
|
|
222926
|
+
mkdirSync20(dirname18(st.screenshot), { recursive: true });
|
|
222927
|
+
mkdirSync20(caseDir, { recursive: true });
|
|
222928
|
+
const finalPng = resolve14(caseDir, "final.png");
|
|
222929
|
+
const { output } = await runDesktopRender(native.js, native.resources, {
|
|
222930
|
+
width: Number(flagStr(args, "width")) || kase.width || projectViewport?.width || 390,
|
|
222931
|
+
height: Number(flagStr(args, "height")) || kase.height || projectViewport?.height || 844,
|
|
222932
|
+
frames: plan.frameCap,
|
|
222933
|
+
outPng: finalPng,
|
|
222934
|
+
logs: flagBool(args, "logs"),
|
|
222935
|
+
timeoutMs: 60000 + plan.steps.length * 5000,
|
|
222936
|
+
script: plan.text,
|
|
222937
|
+
fixedDtMs: nativeFixedDt
|
|
222938
|
+
});
|
|
222939
|
+
const r2 = evaluateDesktopRun(plan, output);
|
|
222940
|
+
if (!r2.loaded)
|
|
222941
|
+
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).");
|
|
222942
|
+
return {
|
|
222943
|
+
name: kase.name,
|
|
222944
|
+
kind: kase.kind,
|
|
222945
|
+
ok: r2.ok,
|
|
222946
|
+
steps: r2.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error, skipped: s2.skipped.length > 0 ? s2.skipped : undefined })),
|
|
222947
|
+
error: r2.ended ? undefined : "the host stopped before the script finished (frame cap / crash)",
|
|
222948
|
+
artifacts: r2.ok ? [] : [finalPng]
|
|
222949
|
+
};
|
|
222950
|
+
};
|
|
222391
222951
|
for (const kase of cases) {
|
|
222392
222952
|
const flagSettle = flagStr(args, "settle");
|
|
222393
222953
|
let result;
|
|
222954
|
+
if (desktop2) {
|
|
222955
|
+
try {
|
|
222956
|
+
result = await runNativeCase(kase);
|
|
222957
|
+
} catch (e2) {
|
|
222958
|
+
if (e2 instanceof CliError && /doesn't support scripted input/.test(e2.message))
|
|
222959
|
+
throw e2;
|
|
222960
|
+
result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
|
|
222961
|
+
}
|
|
222962
|
+
results.push(result);
|
|
222963
|
+
reportCase(result);
|
|
222964
|
+
continue;
|
|
222965
|
+
}
|
|
222394
222966
|
try {
|
|
222395
222967
|
const steps = renderer.parseScenarioSteps(kase.steps);
|
|
222396
222968
|
const session = await renderer.createSession(js, {
|
|
@@ -222404,14 +222976,14 @@ var test = async (args) => {
|
|
|
222404
222976
|
onConsole
|
|
222405
222977
|
});
|
|
222406
222978
|
try {
|
|
222407
|
-
const io = fileScenarioIO(
|
|
222979
|
+
const io = fileScenarioIO(resolve14(artifactsRoot, slug3(kase.name)));
|
|
222408
222980
|
const outcome = await renderer.runScenario(session, steps, io);
|
|
222409
222981
|
result = {
|
|
222410
222982
|
name: kase.name,
|
|
222411
222983
|
kind: kase.kind,
|
|
222412
222984
|
ok: outcome.ok,
|
|
222413
222985
|
steps: outcome.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error })),
|
|
222414
|
-
artifacts: outcome.failureArtifacts.map((p2) =>
|
|
222986
|
+
artifacts: outcome.failureArtifacts.map((p2) => resolve14(artifactsRoot, slug3(kase.name), p2))
|
|
222415
222987
|
};
|
|
222416
222988
|
} finally {
|
|
222417
222989
|
session.dispose();
|
|
@@ -222420,14 +222992,20 @@ var test = async (args) => {
|
|
|
222420
222992
|
result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
|
|
222421
222993
|
}
|
|
222422
222994
|
results.push(result);
|
|
222995
|
+
reportCase(result);
|
|
222996
|
+
}
|
|
222997
|
+
function reportCase(result) {
|
|
222998
|
+
const skippedCount = result.steps.reduce((n2, s2) => n2 + (s2.skipped?.length ?? 0), 0);
|
|
222423
222999
|
if (result.ok) {
|
|
222424
|
-
out(`${c.green("✓")} ${result.name}`);
|
|
223000
|
+
out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} semantic check${skippedCount === 1 ? "" : "s"} skipped on --desktop)`) : ""}`);
|
|
222425
223001
|
} else {
|
|
222426
223002
|
out(`${c.red("✗")} ${result.name}`);
|
|
222427
223003
|
if (result.error)
|
|
222428
223004
|
out(` ${c.red(result.error)}`);
|
|
222429
223005
|
for (const s2 of result.steps) {
|
|
222430
223006
|
out(s2.ok ? c.dim(` ✓ ${s2.desc}`) : ` ${c.red(`✗ ${s2.desc} — ${s2.error}`)}`);
|
|
223007
|
+
for (const sk of s2.skipped ?? [])
|
|
223008
|
+
out(c.dim(` – skipped: ${sk}`));
|
|
222431
223009
|
}
|
|
222432
223010
|
if (result.artifacts.length > 0)
|
|
222433
223011
|
out(c.dim(` failing frame: ${result.artifacts.join(", ")}`));
|
|
@@ -222473,11 +223051,11 @@ init_config();
|
|
|
222473
223051
|
init_manifest();
|
|
222474
223052
|
init_types4();
|
|
222475
223053
|
init_util();
|
|
222476
|
-
import { resolve as
|
|
223054
|
+
import { resolve as resolve15 } from "node:path";
|
|
222477
223055
|
var label = (bundle) => `${Object.keys(bundle).length} file(s)`;
|
|
222478
223056
|
var types4 = async (args) => {
|
|
222479
223057
|
const dirArg = args._[0];
|
|
222480
|
-
const root = dirArg ?
|
|
223058
|
+
const root = dirArg ? resolve15(dirArg) : (() => {
|
|
222481
223059
|
try {
|
|
222482
223060
|
return findProjectRoot(process.cwd());
|
|
222483
223061
|
} catch {
|
|
@@ -222543,16 +223121,16 @@ import { delimiter as delimiter2, dirname as dirname20, join as join29 } from "n
|
|
|
222543
223121
|
// src/commands/install.ts
|
|
222544
223122
|
init_util();
|
|
222545
223123
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
222546
|
-
import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as
|
|
223124
|
+
import { appendFileSync as appendFileSync3, chmodSync as chmodSync2, cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync21, readFileSync as readFileSync27, realpathSync, rmSync as rmSync7, symlinkSync } from "node:fs";
|
|
222547
223125
|
import { homedir as homedir7 } from "node:os";
|
|
222548
|
-
import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as
|
|
223126
|
+
import { basename as basename15, delimiter, dirname as dirname19, join as join28, resolve as resolve16, sep as sep2 } from "node:path";
|
|
222549
223127
|
var isWin = process.platform === "win32";
|
|
222550
223128
|
var HOME = homedir7();
|
|
222551
223129
|
var safeRealpath = (p2) => {
|
|
222552
223130
|
try {
|
|
222553
223131
|
return realpathSync(p2);
|
|
222554
223132
|
} catch {
|
|
222555
|
-
return
|
|
223133
|
+
return resolve16(p2);
|
|
222556
223134
|
}
|
|
222557
223135
|
};
|
|
222558
223136
|
var onPath = (dir) => {
|
|
@@ -222580,7 +223158,7 @@ var install = async (args) => {
|
|
|
222580
223158
|
return installUnix(args, bundle, companions);
|
|
222581
223159
|
};
|
|
222582
223160
|
var copyBundle = (bundle, target, companions) => {
|
|
222583
|
-
|
|
223161
|
+
mkdirSync21(target, { recursive: true });
|
|
222584
223162
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
222585
223163
|
return;
|
|
222586
223164
|
for (const f3 of companions) {
|
|
@@ -222598,8 +223176,8 @@ var permError = (e2, dir, system) => {
|
|
|
222598
223176
|
};
|
|
222599
223177
|
var installUnix = (args, bundle, companions) => {
|
|
222600
223178
|
const system = flagBool(args, "system");
|
|
222601
|
-
const libDir =
|
|
222602
|
-
const binDir =
|
|
223179
|
+
const libDir = resolve16(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join28(HOME, ".local", "lib", "lecodes-cli")));
|
|
223180
|
+
const binDir = resolve16(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join28(HOME, ".local", "bin")));
|
|
222603
223181
|
const link2 = join28(binDir, "lecodes");
|
|
222604
223182
|
try {
|
|
222605
223183
|
copyBundle(bundle, libDir, companions);
|
|
@@ -222609,7 +223187,7 @@ var installUnix = (args, bundle, companions) => {
|
|
|
222609
223187
|
throw permError(e2, libDir, system);
|
|
222610
223188
|
}
|
|
222611
223189
|
try {
|
|
222612
|
-
|
|
223190
|
+
mkdirSync21(binDir, { recursive: true });
|
|
222613
223191
|
rmSync7(link2, { force: true });
|
|
222614
223192
|
symlinkSync(join28(libDir, "lecodes"), link2);
|
|
222615
223193
|
} catch (e2) {
|
|
@@ -222670,7 +223248,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
222670
223248
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
222671
223249
|
};
|
|
222672
223250
|
var installWindows = (args, bundle, companions) => {
|
|
222673
|
-
const target =
|
|
223251
|
+
const target = resolve16(flagStr(args, "dir") ?? join28(process.env.LOCALAPPDATA ?? join28(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
222674
223252
|
try {
|
|
222675
223253
|
copyBundle(bundle, target, companions);
|
|
222676
223254
|
} catch (e2) {
|
|
@@ -222811,8 +223389,12 @@ ${c.bold("Commands:")}
|
|
|
222811
223389
|
--png [file] rasterize a PNG (default screenshot.png;
|
|
222812
223390
|
needs @napi-rs/canvas)
|
|
222813
223391
|
--script <file> drive the app through a step list (tap/type/
|
|
222814
|
-
scroll/wait/expect/screenshot/json steps
|
|
223392
|
+
scroll/wait/expect/screenshot/json steps;
|
|
223393
|
+
games: frames/drag/hold/key/keyDown/keyUp,
|
|
223394
|
+
expect.log, screenshot clip)
|
|
222815
223395
|
--out-dir <dir> where --script step outputs land (default .)
|
|
223396
|
+
--clip <name|x,y,w,h> (--png) crop to one node's rect or a
|
|
223397
|
+
logical-px rect — a component/region render
|
|
222816
223398
|
--fixtures <file> canned fetch responses (URL pattern →
|
|
222817
223399
|
{ status?, json?, text? }) — make
|
|
222818
223400
|
backend-gated screens reachable headless
|
|
@@ -222830,6 +223412,22 @@ ${c.bold("Commands:")}
|
|
|
222830
223412
|
LECODES_DESKTOP_EXE overrides)
|
|
222831
223413
|
--frames <n> (--desktop) frames to run before the shot —
|
|
222832
223414
|
headroom for async loads (default 60)
|
|
223415
|
+
--desktop --script <file> drive the scenario through the native
|
|
223416
|
+
host: tap/drag/hold take { x, y } coordinates
|
|
223417
|
+
(no names — the host has no UI tree; type/
|
|
223418
|
+
scroll/clip unavailable), expect.log is
|
|
223419
|
+
checked, text/node/screen/value are skipped;
|
|
223420
|
+
per-step screenshots + final --png
|
|
223421
|
+
--view <name> (--desktop) inspection camera auto-fitted to the
|
|
223422
|
+
whole 3D scene: iso | top | front | side | back
|
|
223423
|
+
[--fov N]; the app's camera is untouched
|
|
223424
|
+
--camera x,y,z --look-at x,y,z [--fov N]
|
|
223425
|
+
(--desktop) inspection camera at a fixed eye /
|
|
223426
|
+
look-at point (world units, Y up). In a
|
|
223427
|
+
--script run use { "view" } / { "camera" } steps
|
|
223428
|
+
--fixed-dt <ms> (--desktop) virtual app clock, ms per frame —
|
|
223429
|
+
deterministic game time (scripted runs
|
|
223430
|
+
default to 16; plain renders to wall clock)
|
|
222833
223431
|
test [name…] Run headless flow tests: tests/*.flow.json scenarios, plus flows
|
|
222834
223432
|
derived from the design graph (design/meta.json activator edges
|
|
222835
223433
|
— checks the built app against the designed navigation; needs
|
|
@@ -222846,6 +223444,11 @@ ${c.bold("Commands:")}
|
|
|
222846
223444
|
--dir <folder> tests folder (default tests/)
|
|
222847
223445
|
--design-dir <dir> design folder (default design/)
|
|
222848
223446
|
--artifacts <dir> failure-artifact folder override
|
|
223447
|
+
--desktop run the file scenarios through the NATIVE host
|
|
223448
|
+
(coordinates only; expect.log checked,
|
|
223449
|
+
text/node/screen/value skipped; design flows
|
|
223450
|
+
stay headless). --frames / --fixed-dt as in
|
|
223451
|
+
render --desktop
|
|
222849
223452
|
(+ the render timing flags: --width/--height, --settle,
|
|
222850
223453
|
--device, --safe-area, --wait-network, --logs, --entry)
|
|
222851
223454
|
types [dir] Refresh the IDE types (.lecodes/types/ + tsconfig.json) — by
|
|
@@ -222884,7 +223487,9 @@ ${c.bold("Commands:")}
|
|
|
222884
223487
|
app.json fullscreen; --scale <0.5..4|fit> overrides
|
|
222885
223488
|
the display scale — "fit" sizes a 1080p-designed app
|
|
222886
223489
|
to the monitor with the density raised to match;
|
|
222887
|
-
--entry overrides the entrypoint)
|
|
223490
|
+
--entry overrides the entrypoint; asset() resources
|
|
223491
|
+
load from the working tree — --remote-assets or
|
|
223492
|
+
--public-url <url> uses the server's copies instead)
|
|
222888
223493
|
update move to the newest published release
|
|
222889
223494
|
(cached in ~/.lecodes/renderer)
|
|
222890
223495
|
scene [path] LeCodes Scene Editor — visually edit a .scene.ts (tree, inspector,
|