lecodes-cli 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +327 -80
- package/package.json +36 -36
- package/runtime/sdk/inject.ts +1 -0
- package/runtime/sdk/ui/UI.ts +1 -0
- package/runtime/sdk/ui/UIBottomSheet.ts +149 -0
- package/runtime/sdk/ui/UIInput.ts +24 -1
- package/runtime/sdk/ui/UIScrollable.ts +9 -1
- package/runtime/sdk/ui/UIVirtualizedList.ts +4 -1
- package/runtime/sdk/ui/UIWidget.ts +8 -2
- package/runtime/sdk-types.json +1 -1
package/dist/index.js
CHANGED
|
@@ -190,6 +190,23 @@ var resolveInject = () => {
|
|
|
190
190
|
return bin;
|
|
191
191
|
} catch {}
|
|
192
192
|
throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
|
|
193
|
+
}, scanModulesChisel = (files) => {
|
|
194
|
+
let bin;
|
|
195
|
+
try {
|
|
196
|
+
bin = resolveChiselBin();
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const input = JSON.stringify({ scan: true, files });
|
|
201
|
+
const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
202
|
+
if (r.status !== 0 || r.error)
|
|
203
|
+
return null;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse(r.stdout || "{}");
|
|
206
|
+
return parsed.scan ?? null;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
193
210
|
}, bundleProjectChisel = async (entrypoint, files, format, sourcemap, minifyOverride, editor, libraries) => {
|
|
194
211
|
const entry = normalize(entrypoint);
|
|
195
212
|
const slugs = new Set(Object.keys(libraries ?? {}));
|
|
@@ -262,6 +279,79 @@ var init_bundler = __esm(() => {
|
|
|
262
279
|
sdkFilesCache = new Map;
|
|
263
280
|
});
|
|
264
281
|
|
|
282
|
+
// ../sdk/src/compile/detectEntry.ts
|
|
283
|
+
var ENTRY_CONVENTION = "/main.ts", isCode = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts"), normalize2 = (path) => {
|
|
284
|
+
const parts = [];
|
|
285
|
+
for (const part of path.split("/")) {
|
|
286
|
+
if (!part || part === ".")
|
|
287
|
+
continue;
|
|
288
|
+
if (part === "..")
|
|
289
|
+
parts.pop();
|
|
290
|
+
else
|
|
291
|
+
parts.push(part);
|
|
292
|
+
}
|
|
293
|
+
return "/" + parts.join("/");
|
|
294
|
+
}, dirOf = (path) => {
|
|
295
|
+
const i = path.lastIndexOf("/");
|
|
296
|
+
return i <= 0 ? "/" : path.slice(0, i);
|
|
297
|
+
}, resolveSpec = (files, importer, spec) => {
|
|
298
|
+
if (!spec.startsWith("./") && !spec.startsWith("../") && !spec.startsWith("/"))
|
|
299
|
+
return null;
|
|
300
|
+
const base = normalize2(spec.startsWith("/") ? spec : dirOf(importer) + "/" + spec);
|
|
301
|
+
for (const cand of [base, base + ".ts", base + ".js", base + "/index.ts"]) {
|
|
302
|
+
if (cand in files)
|
|
303
|
+
return cand;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}, IMPORT_RE, EXPORT_RE, scanModulesFallback = (files) => {
|
|
307
|
+
const out = {};
|
|
308
|
+
for (const [path, text] of Object.entries(files)) {
|
|
309
|
+
const imports = [];
|
|
310
|
+
for (const m of text.matchAll(IMPORT_RE)) {
|
|
311
|
+
const target = resolveSpec(files, path, m[1] ?? m[2] ?? "");
|
|
312
|
+
if (target && isCode(target) && !imports.includes(target))
|
|
313
|
+
imports.push(target);
|
|
314
|
+
}
|
|
315
|
+
out[path] = { imports, hasExports: EXPORT_RE.test(text) };
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}, detectEntry = (entries, scanner) => {
|
|
319
|
+
const files = {};
|
|
320
|
+
for (const e of entries) {
|
|
321
|
+
if (e.type !== undefined && e.type !== "text")
|
|
322
|
+
continue;
|
|
323
|
+
if (typeof e.text !== "string")
|
|
324
|
+
continue;
|
|
325
|
+
const path = normalize2(e.path.split("\\").join("/"));
|
|
326
|
+
if (isCode(path))
|
|
327
|
+
files[path] = e.text;
|
|
328
|
+
}
|
|
329
|
+
const candidates = Object.keys(files).filter((p) => !p.endsWith(".editor.ts"));
|
|
330
|
+
if (candidates.length === 0)
|
|
331
|
+
return { entry: "", roots: [] };
|
|
332
|
+
const scan = scanner?.(files) ?? scanModulesFallback(files);
|
|
333
|
+
const imported = new Set;
|
|
334
|
+
for (const [path, mod] of Object.entries(scan)) {
|
|
335
|
+
if (path.endsWith(".editor.ts"))
|
|
336
|
+
continue;
|
|
337
|
+
for (const target of mod.imports)
|
|
338
|
+
imported.add(target);
|
|
339
|
+
}
|
|
340
|
+
let roots = candidates.filter((p) => !imported.has(p));
|
|
341
|
+
if (roots.length === 0)
|
|
342
|
+
roots = candidates;
|
|
343
|
+
if (roots.includes(ENTRY_CONVENTION))
|
|
344
|
+
return { entry: ENTRY_CONVENTION, roots };
|
|
345
|
+
const scripts = roots.filter((p) => !scan[p]?.hasExports);
|
|
346
|
+
const pool = scripts.length > 0 ? scripts : roots;
|
|
347
|
+
const bySize = [...pool].sort((a, b) => files[b].length - files[a].length);
|
|
348
|
+
return { entry: bySize[0], roots };
|
|
349
|
+
};
|
|
350
|
+
var init_detectEntry = __esm(() => {
|
|
351
|
+
IMPORT_RE = /(?:^|[\n;])\s*(?:import|export)\b[^'"`]*?\bfrom\s*['"]([^'"\n]+)['"]|(?:^|[\n;])\s*import\s*['"]([^'"\n]+)['"]/g;
|
|
352
|
+
EXPORT_RE = /(?:^|[\n;])\s*export\b/;
|
|
353
|
+
});
|
|
354
|
+
|
|
265
355
|
// ../sdk/src/compile/sourcemap.ts
|
|
266
356
|
var offsetSourceMap = (mapJson, lines) => {
|
|
267
357
|
if (lines <= 0)
|
|
@@ -322,25 +412,10 @@ var buildHeader = (name, code, opts = {}) => {
|
|
|
322
412
|
};
|
|
323
413
|
|
|
324
414
|
// ../sdk/src/compile/compileProject.ts
|
|
325
|
-
var
|
|
326
|
-
|
|
327
|
-
let max = -1;
|
|
328
|
-
for (const e of entries) {
|
|
329
|
-
const ext2 = e.path.split(".").at(-1)?.toLowerCase();
|
|
330
|
-
if (e.path.endsWith(".editor.ts"))
|
|
331
|
-
continue;
|
|
332
|
-
if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
|
|
333
|
-
if (e.text.length > max) {
|
|
334
|
-
entry = e.path;
|
|
335
|
-
max = e.text.length;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
return entry;
|
|
340
|
-
}, compileProject = async (opts) => {
|
|
341
|
-
const entry = opts.entryOverride ?? detectEntry(opts.entries);
|
|
415
|
+
var compileProject = async (opts) => {
|
|
416
|
+
const entry = opts.entryOverride ?? detectEntry(opts.entries, scanModulesChisel).entry;
|
|
342
417
|
if (!entry) {
|
|
343
|
-
throw new Error("Couldn't find an entrypoint (
|
|
418
|
+
throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.");
|
|
344
419
|
}
|
|
345
420
|
const files = {};
|
|
346
421
|
for (const e of opts.entries) {
|
|
@@ -380,6 +455,7 @@ ${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
|
|
|
380
455
|
};
|
|
381
456
|
var init_compileProject = __esm(() => {
|
|
382
457
|
init_bundler();
|
|
458
|
+
init_detectEntry();
|
|
383
459
|
});
|
|
384
460
|
|
|
385
461
|
// ../sdk/src/compile/sceneEditor.ts
|
|
@@ -780,7 +856,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
|
|
|
780
856
|
}
|
|
781
857
|
}
|
|
782
858
|
return null;
|
|
783
|
-
}, norm = (p) => p.replace(/\\/g, "/").replace(/^\//, ""),
|
|
859
|
+
}, norm = (p) => p.replace(/\\/g, "/").replace(/^\//, ""), dirOf2 = (p) => {
|
|
784
860
|
const n = norm(p);
|
|
785
861
|
const i = n.lastIndexOf("/");
|
|
786
862
|
return i === -1 ? "" : n.slice(0, i);
|
|
@@ -823,7 +899,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
|
|
|
823
899
|
const fileFaceCache = new Map;
|
|
824
900
|
const resolveCall = (entryPath, call) => {
|
|
825
901
|
if (call.kind === "file") {
|
|
826
|
-
const filePath = resolveRel(
|
|
902
|
+
const filePath = resolveRel(dirOf2(entryPath), call.arg);
|
|
827
903
|
const target = filePath && entryByPath.get(filePath);
|
|
828
904
|
if (!target)
|
|
829
905
|
throw new Error(`${entryPath}: font("${call.arg}"): no such file in the project (resolved to "${filePath || call.arg}").`);
|
|
@@ -897,7 +973,7 @@ var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <=
|
|
|
897
973
|
}
|
|
898
974
|
for (const fw of work) {
|
|
899
975
|
const entryPath = norm(fw.entry.path);
|
|
900
|
-
const dir =
|
|
976
|
+
const dir = dirOf2(entryPath);
|
|
901
977
|
const modBase = `.${baseOf(entryPath)}.__fonts.ts`;
|
|
902
978
|
const modPath = dir ? `${dir}/${modBase}` : modBase;
|
|
903
979
|
const idents = new Map;
|
|
@@ -961,6 +1037,8 @@ var init_fontMacro = __esm(() => {
|
|
|
961
1037
|
var exports_compile = {};
|
|
962
1038
|
__export(exports_compile, {
|
|
963
1039
|
sceneEditorEntries: () => sceneEditorEntries,
|
|
1040
|
+
scanModulesFallback: () => scanModulesFallback,
|
|
1041
|
+
scanModulesChisel: () => scanModulesChisel,
|
|
964
1042
|
scanFontCalls: () => scanFontCalls,
|
|
965
1043
|
parseSfnt: () => parseSfnt,
|
|
966
1044
|
offsetSourceMap: () => offsetSourceMap,
|
|
@@ -969,6 +1047,7 @@ __export(exports_compile, {
|
|
|
969
1047
|
fontRegistryBase: () => fontRegistryBase,
|
|
970
1048
|
faceFileName: () => faceFileName,
|
|
971
1049
|
detectProjectKind: () => detectProjectKind,
|
|
1050
|
+
detectEntry: () => detectEntry,
|
|
972
1051
|
compileProject: () => compileProject,
|
|
973
1052
|
bundleProjectWithMap: () => bundleProjectWithMap,
|
|
974
1053
|
bundleProject: () => bundleProject,
|
|
@@ -978,10 +1057,12 @@ __export(exports_compile, {
|
|
|
978
1057
|
SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
|
|
979
1058
|
SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
|
|
980
1059
|
FONT_REGISTRY: () => FONT_REGISTRY,
|
|
1060
|
+
ENTRY_CONVENTION: () => ENTRY_CONVENTION,
|
|
981
1061
|
DEFAULT_FONT_REGISTRY_BASE: () => DEFAULT_FONT_REGISTRY_BASE
|
|
982
1062
|
});
|
|
983
1063
|
var init_compile = __esm(() => {
|
|
984
1064
|
init_bundler();
|
|
1065
|
+
init_detectEntry();
|
|
985
1066
|
init_compileProject();
|
|
986
1067
|
init_sceneEditor();
|
|
987
1068
|
init_fontMacro();
|
|
@@ -2007,43 +2088,39 @@ var require_main = __commonJS((exports, module) => {
|
|
|
2007
2088
|
// package.json
|
|
2008
2089
|
var package_default = {
|
|
2009
2090
|
name: "lecodes-cli",
|
|
2010
|
-
version: "0.9.
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
"lecodes",
|
|
2016
|
-
"
|
|
2017
|
-
|
|
2091
|
+
version: "0.9.2",
|
|
2092
|
+
dependencies: {
|
|
2093
|
+
"@letary/chisel": "^0.5.1"
|
|
2094
|
+
},
|
|
2095
|
+
devDependencies: {
|
|
2096
|
+
"lecodes-design": "workspace:*",
|
|
2097
|
+
"lecodes-renderer": "workspace:*",
|
|
2098
|
+
"qrcode-terminal": "^0.12.0",
|
|
2099
|
+
"lecodes-3d-editor": "workspace:*",
|
|
2100
|
+
sdk: "workspace:*"
|
|
2101
|
+
},
|
|
2102
|
+
peerDependencies: {
|
|
2103
|
+
"lecodes-design": "^0.3.2",
|
|
2104
|
+
"lecodes-renderer": "^0.7.1",
|
|
2105
|
+
"lecodes-3d-editor": "^0.1.0"
|
|
2106
|
+
},
|
|
2018
2107
|
bin: {
|
|
2019
2108
|
lecodes: "dist/index.js"
|
|
2020
2109
|
},
|
|
2110
|
+
description: "Clone, edit and push LeCodes projects from your machine.",
|
|
2111
|
+
engines: {
|
|
2112
|
+
node: ">=18"
|
|
2113
|
+
},
|
|
2021
2114
|
files: [
|
|
2022
2115
|
"dist",
|
|
2023
2116
|
"runtime",
|
|
2024
2117
|
"README.md"
|
|
2025
2118
|
],
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
2032
|
-
"sync-peers": "bun scripts/sync-peer-versions.ts",
|
|
2033
|
-
build: 'bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --banner="#!/usr/bin/env node"',
|
|
2034
|
-
"build:bin": "bun scripts/build-bin.ts",
|
|
2035
|
-
typecheck: "tsc --noEmit",
|
|
2036
|
-
dev: "bun run src/index.ts",
|
|
2037
|
-
prepublishOnly: "bun run sync-peers && bun run build"
|
|
2038
|
-
},
|
|
2039
|
-
dependencies: {
|
|
2040
|
-
"@letary/chisel": "^0.4.2"
|
|
2041
|
-
},
|
|
2042
|
-
peerDependencies: {
|
|
2043
|
-
"lecodes-design": "^0.3.2",
|
|
2044
|
-
"lecodes-renderer": "^0.7.0",
|
|
2045
|
-
"lecodes-3d-editor": "^0.1.0"
|
|
2046
|
-
},
|
|
2119
|
+
keywords: [
|
|
2120
|
+
"lecodes",
|
|
2121
|
+
"cli"
|
|
2122
|
+
],
|
|
2123
|
+
license: "MIT",
|
|
2047
2124
|
peerDependenciesMeta: {
|
|
2048
2125
|
"lecodes-design": {
|
|
2049
2126
|
optional: true
|
|
@@ -2055,13 +2132,17 @@ var package_default = {
|
|
|
2055
2132
|
optional: true
|
|
2056
2133
|
}
|
|
2057
2134
|
},
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
"
|
|
2061
|
-
"
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2135
|
+
scripts: {
|
|
2136
|
+
icon: "bun scripts/make-icon.ts",
|
|
2137
|
+
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
2138
|
+
"sync-peers": "bun scripts/sync-peer-versions.ts",
|
|
2139
|
+
build: 'bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --banner="#!/usr/bin/env node"',
|
|
2140
|
+
"build:bin": "bun scripts/build-bin.ts",
|
|
2141
|
+
typecheck: "tsc --noEmit",
|
|
2142
|
+
dev: "bun run src/index.ts",
|
|
2143
|
+
prepublishOnly: "bun run sync-peers && bun run build"
|
|
2144
|
+
},
|
|
2145
|
+
type: "module"
|
|
2065
2146
|
};
|
|
2066
2147
|
|
|
2067
2148
|
// src/commands/app.ts
|
|
@@ -2239,10 +2320,11 @@ var requireApiUrl = (config) => {
|
|
|
2239
2320
|
// src/ignore.ts
|
|
2240
2321
|
import { existsSync as existsSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2241
2322
|
import { join as join3 } from "node:path";
|
|
2242
|
-
var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
|
|
2323
|
+
var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store", ".artifacts/"];
|
|
2243
2324
|
var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
2244
2325
|
# Only untracked files are affected — assets already in the project stay tracked
|
|
2245
|
-
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log
|
|
2326
|
+
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log, .artifacts)
|
|
2327
|
+
# always apply.
|
|
2246
2328
|
|
|
2247
2329
|
# Render / preview outputs (the default \`lecodes render --png\` target)
|
|
2248
2330
|
screenshot.png
|
|
@@ -2651,6 +2733,12 @@ var resolveChiselBin2 = () => {
|
|
|
2651
2733
|
var chiselBin = resolveChiselBin2();
|
|
2652
2734
|
if (chiselBin)
|
|
2653
2735
|
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
2736
|
+
var ambiguousEntryWarning = (entries) => {
|
|
2737
|
+
const { entry, roots } = detectEntry(entries, scanModulesChisel);
|
|
2738
|
+
if (!entry || roots.length < 2 || roots.includes(ENTRY_CONVENTION))
|
|
2739
|
+
return null;
|
|
2740
|
+
return `Ambiguous entrypoint: nothing imports ${roots.join(", ")} — picked ${entry}. Rename your entry to main.ts (or pass --entry).`;
|
|
2741
|
+
};
|
|
2654
2742
|
var compileProject2 = (opts) => compileProject({
|
|
2655
2743
|
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders })),
|
|
2656
2744
|
name: opts.name,
|
|
@@ -2846,11 +2934,17 @@ var compileImpl = async (args, shell) => {
|
|
|
2846
2934
|
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURI(path), absPath })
|
|
2847
2935
|
});
|
|
2848
2936
|
note("Compiling…");
|
|
2937
|
+
const entryFlag = flagStr(args, "entry");
|
|
2938
|
+
if (!entryFlag) {
|
|
2939
|
+
const ambiguous = ambiguousEntryWarning(entries);
|
|
2940
|
+
if (ambiguous)
|
|
2941
|
+
warnErr(ambiguous);
|
|
2942
|
+
}
|
|
2849
2943
|
const js = await compileProject2({
|
|
2850
2944
|
entries,
|
|
2851
2945
|
name,
|
|
2852
2946
|
publicUrl,
|
|
2853
|
-
entryOverride:
|
|
2947
|
+
entryOverride: entryFlag,
|
|
2854
2948
|
header: !flagBool(args, "no-header"),
|
|
2855
2949
|
localAssets,
|
|
2856
2950
|
fontsHeader,
|
|
@@ -4322,6 +4416,73 @@ var clone = async (args) => {
|
|
|
4322
4416
|
// src/commands/create.ts
|
|
4323
4417
|
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4324
4418
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
4419
|
+
|
|
4420
|
+
// src/commands/projectTemplates.ts
|
|
4421
|
+
var appClaudeMd = (name) => `# ${name} — LeCodes app
|
|
4422
|
+
|
|
4423
|
+
Guidance for AI assistants working in this folder. Human docs: README.md. The whole SDK is
|
|
4424
|
+
available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) — no SDK imports. \`main.ts\` is
|
|
4425
|
+
the entry point; split code into other \`.ts\` files and import them (relative imports only) —
|
|
4426
|
+
a file no other file imports is treated as an entry candidate, so keep everything reachable
|
|
4427
|
+
from \`main.ts\`.
|
|
4428
|
+
|
|
4429
|
+
## Feedback loop — run the app headless instead of guessing
|
|
4430
|
+
|
|
4431
|
+
\`\`\`
|
|
4432
|
+
lecodes render # semantic JSON of the launched UI (structure, text, names, values)
|
|
4433
|
+
lecodes render --png shot.png # pixels (needs @napi-rs/canvas installed)
|
|
4434
|
+
lecodes render --script <file> # drive the app through a scenario (format below)
|
|
4435
|
+
lecodes test # all tests/*.flow.json + design-derived flows; run before "done"
|
|
4436
|
+
\`\`\`
|
|
4437
|
+
|
|
4438
|
+
Iterate with \`--script\` scenarios and keep the useful ones as \`tests/<name>.flow.json\` — they
|
|
4439
|
+
are the regression suite. A failing step dumps the failing frame (JSON + PNG) under
|
|
4440
|
+
\`tests/.artifacts/\` so a red step explains itself.
|
|
4441
|
+
|
|
4442
|
+
## Scenario format (tests/*.flow.json and --script files)
|
|
4443
|
+
|
|
4444
|
+
One action per step (\`tap\`, \`type\`+\`text\`, \`fill\`, \`scroll\` [+\`by\`], \`wait\`), then an
|
|
4445
|
+
optional \`expect\` (\`screen\`, \`node\`/\`notNode\`, \`text\`/\`notText\`,
|
|
4446
|
+
\`value: { target, equals }\`), then outputs (\`screenshot\`, \`json\`). Targets resolve by
|
|
4447
|
+
\`name\` → exact visible text → substring → input placeholder. Envelope keys: \`name\`,
|
|
4448
|
+
\`width\`/\`height\`, \`settle\`, \`fixtures\`, \`steps\`.
|
|
4449
|
+
|
|
4450
|
+
\`\`\`json
|
|
4451
|
+
{ "steps": [
|
|
4452
|
+
{ "expect": { "screen": "home", "text": "Taps: 0" } },
|
|
4453
|
+
{ "tap": "tap", "expect": { "text": "Taps: 1" }, "screenshot": "after.png" }
|
|
4454
|
+
] }
|
|
4455
|
+
\`\`\`
|
|
4456
|
+
|
|
4457
|
+
## Make the app testable
|
|
4458
|
+
|
|
4459
|
+
- Give interactive elements a \`name\`: \`UIButton(...).style({ name: "login" })\`.
|
|
4460
|
+
- Name every screen's ROOT node with the screen's id — \`expect.screen\` reads it.
|
|
4461
|
+
- If the project has a \`design/\` folder, keep the design's screen ids and activator \`name\`s
|
|
4462
|
+
in the app code: \`lecodes test\` then checks the built app against the designed navigation
|
|
4463
|
+
automatically (conventions in design/CLAUDE.md).
|
|
4464
|
+
- Backend-gated screens: canned responses in \`tests/fixtures.json\` (URL pattern →
|
|
4465
|
+
\`{ status?, json?, text? }\`; \`*\` wildcards full-match, plain strings substring-match, first
|
|
4466
|
+
match wins). \`lecodes test\` auto-loads it for design-derived flows; a scenario file opts in
|
|
4467
|
+
with \`"fixtures": "fixtures.json"\` in its envelope.
|
|
4468
|
+
- \`fill\` fills every empty input — use it before tapping a disabled-until-filled submit.
|
|
4469
|
+
|
|
4470
|
+
## Other commands
|
|
4471
|
+
|
|
4472
|
+
\`lecodes dev\` (run on a device with hot reload + QR), \`lecodes compile -o app.js\`
|
|
4473
|
+
(single-file bundle), \`lecodes design\` (screen-design board — the living spec).
|
|
4474
|
+
`;
|
|
4475
|
+
var STARTER_FLOW = `{
|
|
4476
|
+
"name": "counter",
|
|
4477
|
+
"steps": [
|
|
4478
|
+
{ "expect": { "screen": "home", "text": "Taps: 0" } },
|
|
4479
|
+
{ "tap": "tap", "expect": { "text": "Taps: 1" } },
|
|
4480
|
+
{ "tap": "tap", "expect": { "text": "Taps: 2" } }
|
|
4481
|
+
]
|
|
4482
|
+
}
|
|
4483
|
+
`;
|
|
4484
|
+
|
|
4485
|
+
// src/commands/create.ts
|
|
4325
4486
|
var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
4326
4487
|
var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
|
|
4327
4488
|
const scene = new Scene2D({ background: '#10131a' })
|
|
@@ -4351,6 +4512,7 @@ var create = async (args) => {
|
|
|
4351
4512
|
writeTsconfig(root);
|
|
4352
4513
|
writeDefaultIgnore(root);
|
|
4353
4514
|
writeFileSync9(join13(root, "main.ts"), STARTER);
|
|
4515
|
+
writeFileSync9(join13(root, "CLAUDE.md"), appClaudeMd(name));
|
|
4354
4516
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
4355
4517
|
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
|
|
4356
4518
|
success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
|
|
@@ -5088,6 +5250,10 @@ contract instead of a discarded sketch — but only if the app build keeps the w
|
|
|
5088
5250
|
machine-verifiable* — cover the important ones with hand-written \`tests/*.flow.json\` scenarios
|
|
5089
5251
|
(same step schema as \`lecodes render --script\`; tab-bar navigation needs these too, since the
|
|
5090
5252
|
bar draws no edges).
|
|
5253
|
+
- Backend-gated transitions (a submit that must succeed to navigate) run headless through
|
|
5254
|
+
\`tests/fixtures.json\` — canned API responses (URL pattern → \`{ status?, json?, text? }\`)
|
|
5255
|
+
that \`lecodes test\` loads automatically for the derived flows. Shape them after the app's
|
|
5256
|
+
API types; the spec.md data model is their first draft.
|
|
5091
5257
|
|
|
5092
5258
|
Names also appear in every headless render (JSON snapshots, the board's live elements), so they
|
|
5093
5259
|
double as stable selectors for any scripted interaction — naming interactive elements is never
|
|
@@ -6003,6 +6169,11 @@ var startDevServer = async (options) => {
|
|
|
6003
6169
|
publicUrl: origin,
|
|
6004
6170
|
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: `${base}/assets` + encodeURI(path), absPath })
|
|
6005
6171
|
});
|
|
6172
|
+
if (!entryOverride) {
|
|
6173
|
+
const ambiguous = ambiguousEntryWarning(entries);
|
|
6174
|
+
if (ambiguous)
|
|
6175
|
+
warnings.push(ambiguous);
|
|
6176
|
+
}
|
|
6006
6177
|
const js = await compileProject2({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader });
|
|
6007
6178
|
const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs);
|
|
6008
6179
|
if (changed) {
|
|
@@ -6494,10 +6665,12 @@ const button = UIButton(
|
|
|
6494
6665
|
justifyContent: "center",
|
|
6495
6666
|
},
|
|
6496
6667
|
[UIText({ color: "#ffffff", fontSize: 22 }, "Tap me")],
|
|
6497
|
-
)
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6668
|
+
)
|
|
6669
|
+
.style({ name: "tap" }) // names make elements targetable in tests/*.flow.json (see CLAUDE.md)
|
|
6670
|
+
.onClick(() => {
|
|
6671
|
+
count++
|
|
6672
|
+
counter.text = "Taps: " + count
|
|
6673
|
+
})
|
|
6501
6674
|
|
|
6502
6675
|
const screen = UIScreen(
|
|
6503
6676
|
{ backgroundColor: "#1e2230", alignItems: "center", justifyContent: "center", padding: 40 },
|
|
@@ -6507,7 +6680,7 @@ const screen = UIScreen(
|
|
|
6507
6680
|
[UIText({ color: "#ffffff", fontSize: 32 }, "Hello, LeCodes"), button, counter],
|
|
6508
6681
|
),
|
|
6509
6682
|
],
|
|
6510
|
-
)
|
|
6683
|
+
).style({ name: "home" }) // the screen root's name is what tests' \`expect: { screen: … }\` reads
|
|
6511
6684
|
|
|
6512
6685
|
screen.open()
|
|
6513
6686
|
`;
|
|
@@ -6523,6 +6696,16 @@ lecodes dev # live on your phone: scan the QR with the LeCodes
|
|
|
6523
6696
|
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
6524
6697
|
\`\`\`
|
|
6525
6698
|
|
|
6699
|
+
## Test it (no device needed)
|
|
6700
|
+
|
|
6701
|
+
\`\`\`
|
|
6702
|
+
lecodes render --png shot.png # headless screenshot of the running app
|
|
6703
|
+
lecodes test # run tests/*.flow.json — the starter ships with a passing one
|
|
6704
|
+
\`\`\`
|
|
6705
|
+
|
|
6706
|
+
Scenario format + conventions: see [CLAUDE.md](./CLAUDE.md) (written for AI assistants, useful
|
|
6707
|
+
for humans too).
|
|
6708
|
+
|
|
6526
6709
|
## Go native
|
|
6527
6710
|
|
|
6528
6711
|
\`\`\`
|
|
@@ -6533,8 +6716,11 @@ lecodes app open # open it in Xcode, build & run
|
|
|
6533
6716
|
|
|
6534
6717
|
## Layout
|
|
6535
6718
|
|
|
6536
|
-
- \`main.ts\` — the app. Add more
|
|
6537
|
-
is
|
|
6719
|
+
- \`main.ts\` — the app's entry point. Add more .ts files freely and import them from
|
|
6720
|
+
\`main.ts\`; a file named \`main.ts\` is always the entry.
|
|
6721
|
+
- \`tests/\` — headless flow tests (\`lecodes test\`); failing steps dump the frame to
|
|
6722
|
+
\`tests/.artifacts/\`
|
|
6723
|
+
- \`CLAUDE.md\` — guidance for AI assistants (the headless feedback loop + testability conventions)
|
|
6538
6724
|
- \`assets/\` — images/fonts the app references (created on demand)
|
|
6539
6725
|
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit)
|
|
6540
6726
|
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
@@ -6560,6 +6746,8 @@ var init2 = async (args) => {
|
|
|
6560
6746
|
};
|
|
6561
6747
|
write("main.ts", STARTER2);
|
|
6562
6748
|
write("README.md", readmeTemplate(name));
|
|
6749
|
+
write("CLAUDE.md", appClaudeMd(name));
|
|
6750
|
+
write("tests/counter.flow.json", STARTER_FLOW);
|
|
6563
6751
|
write(".lecodesignore", DEFAULT_IGNORE_FILE);
|
|
6564
6752
|
const wroteTypes = materializeTypesLocal(root);
|
|
6565
6753
|
writeTsconfig(root);
|
|
@@ -6571,6 +6759,7 @@ var init2 = async (args) => {
|
|
|
6571
6759
|
success(created ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.` : `Project ${c.bold(name)} already initialized.`);
|
|
6572
6760
|
const cd = dirArg === "." ? "" : `cd ${dirArg} && `;
|
|
6573
6761
|
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
|
|
6762
|
+
log(` ${c.bold(cd + "lecodes test")} run the flow tests headless (no device)`);
|
|
6574
6763
|
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
|
|
6575
6764
|
};
|
|
6576
6765
|
|
|
@@ -6635,11 +6824,17 @@ var compileHeadlessBundle = async (args) => {
|
|
|
6635
6824
|
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath })
|
|
6636
6825
|
} : { mode: "cdn", root });
|
|
6637
6826
|
note("Compiling…");
|
|
6827
|
+
const entryFlag = flagStr(args, "entry");
|
|
6828
|
+
if (!entryFlag) {
|
|
6829
|
+
const ambiguous = ambiguousEntryWarning(compileEntries);
|
|
6830
|
+
if (ambiguous)
|
|
6831
|
+
warnings.push(ambiguous);
|
|
6832
|
+
}
|
|
6638
6833
|
const js = await compileProject2({
|
|
6639
6834
|
entries: compileEntries,
|
|
6640
6835
|
name,
|
|
6641
6836
|
publicUrl,
|
|
6642
|
-
entryOverride:
|
|
6837
|
+
entryOverride: entryFlag,
|
|
6643
6838
|
header: false,
|
|
6644
6839
|
format: "iife"
|
|
6645
6840
|
});
|
|
@@ -6687,6 +6882,23 @@ var fileScenarioIO = (dir) => ({
|
|
|
6687
6882
|
writeFileSync14(abs, data);
|
|
6688
6883
|
}
|
|
6689
6884
|
});
|
|
6885
|
+
var resolveFixtures = (value, baseDir) => {
|
|
6886
|
+
if (value === undefined)
|
|
6887
|
+
return;
|
|
6888
|
+
let map = value;
|
|
6889
|
+
if (typeof value === "string") {
|
|
6890
|
+
const abs = resolve6(baseDir, value);
|
|
6891
|
+
try {
|
|
6892
|
+
map = JSON.parse(readFileSync17(abs, "utf8"));
|
|
6893
|
+
} catch (e) {
|
|
6894
|
+
throw new CliError(`Can't read fixtures "${value}": ${e instanceof Error ? e.message : String(e)}`);
|
|
6895
|
+
}
|
|
6896
|
+
}
|
|
6897
|
+
if (typeof map !== "object" || map === null || Array.isArray(map)) {
|
|
6898
|
+
throw new CliError(`Fixtures must be an object mapping URL patterns to { status?, json?, text? }.`);
|
|
6899
|
+
}
|
|
6900
|
+
return map;
|
|
6901
|
+
};
|
|
6690
6902
|
var readScenarioFile = (path) => {
|
|
6691
6903
|
let raw;
|
|
6692
6904
|
try {
|
|
@@ -6720,10 +6932,21 @@ var render = async (args) => {
|
|
|
6720
6932
|
const settleStr = flagStr(args, "settle") ?? (scenario?.settle !== undefined ? String(scenario.settle) : undefined);
|
|
6721
6933
|
const timeStr = flagStr(args, "time");
|
|
6722
6934
|
const onConsole = flagBool(args, "logs") ? (level, text2) => logErr(`${c.dim(`[${level}]`)} ${text2}`) : undefined;
|
|
6935
|
+
const envelopeFixtures = resolveFixtures(scenario?.fixtures, scriptFlag !== undefined ? dirname11(resolve6(scriptFlag)) : ".");
|
|
6936
|
+
const flagFixtures = resolveFixtures(flagStr(args, "fixtures"), ".");
|
|
6937
|
+
const fixtures = envelopeFixtures || flagFixtures ? { ...envelopeFixtures, ...flagFixtures } : undefined;
|
|
6938
|
+
if (fixtures) {
|
|
6939
|
+
try {
|
|
6940
|
+
renderer.validateFixtures(fixtures);
|
|
6941
|
+
} catch (e) {
|
|
6942
|
+
throw new CliError(e instanceof Error ? e.message : String(e));
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6723
6945
|
const timing = {
|
|
6724
6946
|
settleMs: settleStr !== undefined ? Number(settleStr) : undefined,
|
|
6725
6947
|
waitNetwork: flagBool(args, "wait-network"),
|
|
6726
6948
|
localAssets: useLocalAssets,
|
|
6949
|
+
fixtures,
|
|
6727
6950
|
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
6728
6951
|
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
6729
6952
|
...resolveSafeAreaChoice(flagStr(args, "safe-area") ?? scenario?.safeArea, flagStr(args, "device") ?? scenario?.device),
|
|
@@ -7172,7 +7395,7 @@ var status = async (_args) => {
|
|
|
7172
7395
|
|
|
7173
7396
|
// src/commands/test.ts
|
|
7174
7397
|
import { existsSync as existsSync20, readFileSync as readFileSync21, readdirSync as readdirSync9 } from "node:fs";
|
|
7175
|
-
import { resolve as resolve8 } from "node:path";
|
|
7398
|
+
import { dirname as dirname14, resolve as resolve8 } from "node:path";
|
|
7176
7399
|
var deriveDesignFlows = (meta, entryScreen) => {
|
|
7177
7400
|
const edges = meta.edges ?? [];
|
|
7178
7401
|
const isState = (id) => id.includes("@");
|
|
@@ -7221,7 +7444,8 @@ var deriveDesignFlows = (meta, entryScreen) => {
|
|
|
7221
7444
|
name,
|
|
7222
7445
|
kind: "flow",
|
|
7223
7446
|
steps: [
|
|
7224
|
-
...path.
|
|
7447
|
+
...path.flatMap((hop) => [{ fill: {} }, { tap: hop.activator, expect: { screen: hop.to } }]),
|
|
7448
|
+
{ fill: {} },
|
|
7225
7449
|
{ tap: e.activator, expect: { screen: e.to } }
|
|
7226
7450
|
]
|
|
7227
7451
|
});
|
|
@@ -7242,7 +7466,17 @@ var readFlowFile = (abs, displayName) => {
|
|
|
7242
7466
|
throw new CliError(`"${displayName}" must be a JSON object with "steps" (or a bare step array).`);
|
|
7243
7467
|
}
|
|
7244
7468
|
const file = raw;
|
|
7245
|
-
return {
|
|
7469
|
+
return {
|
|
7470
|
+
name: file.name ?? displayName,
|
|
7471
|
+
kind: "file",
|
|
7472
|
+
steps: file.steps,
|
|
7473
|
+
fixtures: resolveFixtures(file.fixtures, dirname14(abs)),
|
|
7474
|
+
width: file.width,
|
|
7475
|
+
height: file.height,
|
|
7476
|
+
settle: file.settle,
|
|
7477
|
+
device: file.device,
|
|
7478
|
+
safeArea: file.safeArea
|
|
7479
|
+
};
|
|
7246
7480
|
};
|
|
7247
7481
|
var test = async (args) => {
|
|
7248
7482
|
const { js, warnings, useLocalAssets, root } = await compileHeadlessBundle(args);
|
|
@@ -7265,6 +7499,9 @@ var test = async (args) => {
|
|
|
7265
7499
|
cases.push(readFlowFile(resolve8(testsDir, f2), f2));
|
|
7266
7500
|
}
|
|
7267
7501
|
}
|
|
7502
|
+
const fixturesFlag = flagStr(args, "fixtures");
|
|
7503
|
+
const defaultPack = resolve8(testsDir, "fixtures.json");
|
|
7504
|
+
const flowFixtures = fixturesFlag !== undefined ? resolveFixtures(fixturesFlag, ".") : existsSync20(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
7268
7505
|
const flowsOnly = flagBool(args, "flows");
|
|
7269
7506
|
const wantFlows = !flagBool(args, "no-flows") && args._.length === 0;
|
|
7270
7507
|
if (flowsOnly)
|
|
@@ -7280,14 +7517,14 @@ var test = async (args) => {
|
|
|
7280
7517
|
}
|
|
7281
7518
|
if ((meta.edges ?? []).length > 0) {
|
|
7282
7519
|
note("Probing the start screen…");
|
|
7283
|
-
const probe = await renderer.createSession(js, { raster: false, localAssets: useLocalAssets });
|
|
7520
|
+
const probe = await renderer.createSession(js, { raster: false, localAssets: useLocalAssets, fixtures: flowFixtures });
|
|
7284
7521
|
entryScreen = probe.screenName();
|
|
7285
7522
|
probe.dispose();
|
|
7286
7523
|
if (entryScreen === null) {
|
|
7287
7524
|
warnErr('Design flows skipped: the app\'s start screen root has no `name`. Name each screen root with its design screen id (e.g. UIScreen([...], { name: "login" })).');
|
|
7288
7525
|
} else {
|
|
7289
7526
|
const derived = deriveDesignFlows(meta, entryScreen);
|
|
7290
|
-
cases.push(...derived.flows);
|
|
7527
|
+
cases.push(...derived.flows.map((f2) => ({ ...f2, fixtures: flowFixtures })));
|
|
7291
7528
|
skipped = derived.skipped;
|
|
7292
7529
|
}
|
|
7293
7530
|
}
|
|
@@ -7319,6 +7556,7 @@ var test = async (args) => {
|
|
|
7319
7556
|
settleMs: flagSettle !== undefined ? Number(flagSettle) : kase.settle,
|
|
7320
7557
|
waitNetwork: flagBool(args, "wait-network"),
|
|
7321
7558
|
localAssets: useLocalAssets,
|
|
7559
|
+
fixtures: kase.fixtures,
|
|
7322
7560
|
...resolveSafeAreaChoice(flagStr(args, "safe-area") ?? kase.safeArea, flagStr(args, "device") ?? kase.device),
|
|
7323
7561
|
onConsole
|
|
7324
7562
|
});
|
|
@@ -7389,13 +7627,13 @@ var test = async (args) => {
|
|
|
7389
7627
|
// src/index.ts
|
|
7390
7628
|
import { existsSync as existsSync22, readFileSync as readFileSync23, realpathSync as realpathSync2 } from "node:fs";
|
|
7391
7629
|
import { createRequire as createRequire4 } from "node:module";
|
|
7392
|
-
import { delimiter as delimiter2, dirname as
|
|
7630
|
+
import { delimiter as delimiter2, dirname as dirname16, join as join23 } from "node:path";
|
|
7393
7631
|
|
|
7394
7632
|
// src/commands/install.ts
|
|
7395
7633
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7396
7634
|
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as existsSync21, mkdirSync as mkdirSync15, readFileSync as readFileSync22, realpathSync, rmSync as rmSync3, symlinkSync } from "node:fs";
|
|
7397
7635
|
import { homedir as homedir3 } from "node:os";
|
|
7398
|
-
import { basename as basename12, delimiter, dirname as
|
|
7636
|
+
import { basename as basename12, delimiter, dirname as dirname15, join as join22, resolve as resolve9, sep } from "node:path";
|
|
7399
7637
|
var isWin = process.platform === "win32";
|
|
7400
7638
|
var HOME = homedir3();
|
|
7401
7639
|
var safeRealpath = (p) => {
|
|
@@ -7418,7 +7656,7 @@ var onPath = (dir) => {
|
|
|
7418
7656
|
});
|
|
7419
7657
|
};
|
|
7420
7658
|
var install = async (args) => {
|
|
7421
|
-
const bundle =
|
|
7659
|
+
const bundle = dirname15(process.execPath);
|
|
7422
7660
|
const exeName = basename12(process.execPath);
|
|
7423
7661
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
7424
7662
|
if (!existsSync21(join22(bundle, "runtime")) || !existsSync21(join22(bundle, chiselName))) {
|
|
@@ -7557,7 +7795,7 @@ var addToUserPathWindows = (dir) => {
|
|
|
7557
7795
|
|
|
7558
7796
|
// src/index.ts
|
|
7559
7797
|
var STANDALONE = (() => {
|
|
7560
|
-
const dir =
|
|
7798
|
+
const dir = dirname16(process.execPath);
|
|
7561
7799
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
7562
7800
|
return existsSync22(join23(dir, "runtime")) && existsSync22(join23(dir, chisel));
|
|
7563
7801
|
})();
|
|
@@ -7594,7 +7832,8 @@ ${c.bold("Usage:")} lecodes <command> [options]
|
|
|
7594
7832
|
|
|
7595
7833
|
${c.bold("Commands:")}
|
|
7596
7834
|
init [dir] Scaffold a new LOCAL project (no login/server): a starter
|
|
7597
|
-
main.ts,
|
|
7835
|
+
main.ts, a CLAUDE.md + passing flow test (the headless
|
|
7836
|
+
render/test loop), IDE types + tsconfig, a .lecodesignore.
|
|
7598
7837
|
Run it with 'lecodes dev' or 'lecodes compile'.${INSTALL_HELP}
|
|
7599
7838
|
login Authorize in the browser and store an access token
|
|
7600
7839
|
--token <pat> store a token from the web UI instead
|
|
@@ -7641,6 +7880,9 @@ ${c.bold("Commands:")}
|
|
|
7641
7880
|
--script <file> drive the app through a step list (tap/type/
|
|
7642
7881
|
scroll/wait/expect/screenshot/json steps)
|
|
7643
7882
|
--out-dir <dir> where --script step outputs land (default .)
|
|
7883
|
+
--fixtures <file> canned fetch responses (URL pattern →
|
|
7884
|
+
{ status?, json?, text? }) — make
|
|
7885
|
+
backend-gated screens reachable headless
|
|
7644
7886
|
--scale <n> PNG device-pixel scale (default 2)
|
|
7645
7887
|
--width/--height viewport size (default 390x844)
|
|
7646
7888
|
--entry <path> override the entrypoint
|
|
@@ -7652,8 +7894,13 @@ ${c.bold("Commands:")}
|
|
|
7652
7894
|
— checks the built app against the designed navigation; needs
|
|
7653
7895
|
named elements + screen roots). Failing steps dump the frame
|
|
7654
7896
|
(JSON + PNG) under tests/.artifacts/<flow>/.
|
|
7897
|
+
Design flows read tests/fixtures.json automatically (canned
|
|
7898
|
+
fetch responses) so backend-gated transitions run headless;
|
|
7899
|
+
file scenarios declare their own via the "fixtures" envelope.
|
|
7655
7900
|
--flows run ONLY the design-derived flows
|
|
7656
7901
|
--no-flows run ONLY the tests/*.flow.json files
|
|
7902
|
+
--fixtures <file> fixture pack for the design flows (overrides
|
|
7903
|
+
the tests/fixtures.json default)
|
|
7657
7904
|
--json machine-readable report on stdout
|
|
7658
7905
|
--dir <folder> tests folder (default tests/)
|
|
7659
7906
|
--design-dir <dir> design folder (default design/)
|
|
@@ -7720,7 +7967,7 @@ var OPTIONAL_PEERS = [
|
|
|
7720
7967
|
var peerVersion = (name, probe) => {
|
|
7721
7968
|
try {
|
|
7722
7969
|
const require2 = createRequire4(import.meta.url);
|
|
7723
|
-
let dir =
|
|
7970
|
+
let dir = dirname16(require2.resolve(probe));
|
|
7724
7971
|
for (let depth = 0;depth < 16; depth++) {
|
|
7725
7972
|
const manifest = join23(dir, "package.json");
|
|
7726
7973
|
if (existsSync22(manifest)) {
|
|
@@ -7728,7 +7975,7 @@ var peerVersion = (name, probe) => {
|
|
|
7728
7975
|
if (parsed.name === name)
|
|
7729
7976
|
return parsed.version ?? "unknown";
|
|
7730
7977
|
}
|
|
7731
|
-
const parent =
|
|
7978
|
+
const parent = dirname16(dir);
|
|
7732
7979
|
if (parent === dir)
|
|
7733
7980
|
break;
|
|
7734
7981
|
dir = parent;
|