@loadmap/cli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/bin.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-CKZ8ncFf.js → src-KVw61wOu.js} +154 -29
- package/dist/viewer/assets/index-BhXYID7c.js +287 -0
- package/dist/viewer/index.html +1 -1
- package/package.json +14 -6
- package/dist/viewer/assets/index-B1V1dQ1U.js +0 -187
package/README.md
CHANGED
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
```bash
|
|
4
4
|
npx @loadmap/cli check shop.loadmap # exit 1 when an expect line fails; GitHub annotations under Actions
|
|
5
5
|
npx @loadmap/cli fmt shop.loadmap # lays it out the one way; --check exits 1 instead (for CI)
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
npm i -D @loadmap/cli playwright && npx playwright install chromium # once, to record
|
|
8
|
+
npx loadmap record shop.loadmap --out shop.gif --caption
|
|
9
|
+
npx loadmap record http://localhost:5173/ --out poster.png --poster
|
|
8
10
|
```
|
|
9
11
|
|
|
10
12
|
`loadmap check` compiles each `.loadmap` and tests its `expect` lines on the pessimistic edge (every capacity at its
|
|
11
13
|
low bound). `loadmap fmt` rewrites each file laid out the one way (two spaces a block, a statement a line, one space
|
|
12
14
|
around operators; numbers, texts and formulas as written), `--check` only names the ones that are not, `-` reads
|
|
13
15
|
standard input. `loadmap record` draws a `.loadmap` (or opens a page that calls `expose()` from `@loadmap/export`) in
|
|
14
|
-
headless Chromium and records it on a fixed clock: the same options give the same file.
|
|
15
|
-
|
|
16
|
+
headless Chromium and records it on a fixed clock: the same options give the same file. Only recording needs
|
|
17
|
+
Playwright and its Chromium, installed next to the command (`npx @loadmap/cli record` alone cannot find them);
|
|
18
|
+
`check` and `fmt` run without them. `loadmap --help` lists every option.
|
|
16
19
|
Docs: https://jaim3r0jas.github.io/loadmap/ · MIT
|
package/dist/bin.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ interface FmtOptions {
|
|
|
40
40
|
* file has a syntax error, cannot be read, or (with --check) needs formatting; 2 for a usage mistake.
|
|
41
41
|
*/
|
|
42
42
|
export declare function fmt(argv: string[], o?: FmtOptions): Promise<number>;
|
|
43
|
-
export declare const USAGE = "loadmap check <file.loadmap>… [--format text|github]\nloadmap fmt <file.loadmap>… [--check] | loadmap fmt -\nloadmap record <url | file.loadmap> --out <file.gif|.webm|.mp4|.png|.svg> [options]\n\ncheck compiles each definition and tests its expect lines on the pessimistic edge (every capacity at its low\nbound). It exits 1 when a definition has an error or an expectation fails, 0 when everything holds.\n\n --format <f> text, or github to add annotations on the lines (the default under GitHub Actions)\n\nfmt lays each definition out the one way, in place: two spaces a block, a statement a line, one space around\noperators; numbers, texts and formulas stay as written. A file with a syntax error is left alone (exit 1).\n\n --check change nothing; exit 1 when a file is not laid out so (for CI)\n - read standard input, write standard output\n\nrecord opens the page in headless Chromium, finds the loadmap it exposes (expose() from @loadmap/export) and\nrecords it on a fixed clock, so the same options give the same file. A .loadmap file is drawn by the viewer that\ncomes with the command.\n\n --out <file> the extension picks the format: gif, webm, mp4, png or svg\n --script <file.json> [{ \"at\": 0, \"set\": { \"load\": 5 } }, { \"at\": 3, \"scenario\": \"Proposed\" }, …]\n or { \"seconds\", \"fps\", \"script\": […] }\n --scenario <label> start from a scenario's values (--set goes on top)\n --set <key=value> a value before recording starts; repeatable (--set load=30 --set pods=6)\n --caption the title, scenario and load above the board, the verdict below it\n --story play the definition's story, a step at a time (with --caption, its text below the board)\n --poster with .png: the board, the verdict, the limits and the scenarios compared\n --seconds <n> length of a video or GIF (default 8, or the script's)\n --fps <n> frames per second (default 30; GIF 15, at most 50)\n --scale <n> pixels per diagram unit (default 1, PNG 2)\n --seed <n> the sim's seed (default 1)\n --name <name> which exposed loadmap, when the page has several (default \"default\")\n --width <px> the browser window (default 1280; below 640 the diagram runs top to bottom)\n --color-scheme <s> light or dark, for a page that follows the system theme (default light)";
|
|
43
|
+
export declare const USAGE = "loadmap check <file.loadmap>… [--format text|github]\nloadmap fmt <file.loadmap>… [--check] | loadmap fmt -\nloadmap record <url | file.loadmap> --out <file.gif|.webm|.mp4|.png|.svg> [options]\n\ncheck compiles each definition and tests its expect lines on the pessimistic edge (every capacity at its low\nbound). It exits 1 when a definition has an error or an expectation fails, 0 when everything holds.\n\n --format <f> text, or github to add annotations on the lines (the default under GitHub Actions)\n\nfmt lays each definition out the one way, in place: two spaces a block, a statement a line, one space around\noperators; numbers, texts and formulas stay as written. A file with a syntax error is left alone (exit 1).\n\n --check change nothing; exit 1 when a file is not laid out so (for CI)\n - read standard input, write standard output\n\nrecord opens the page in headless Chromium, finds the loadmap it exposes (expose() from @loadmap/export) and\nrecords it on a fixed clock, so the same options give the same file. A .loadmap file is drawn by the viewer that\ncomes with the command. It needs Playwright next to the command: npm i -D playwright, then npx playwright install\nchromium (check and fmt run without it).\n\n --out <file> the extension picks the format: gif, webm, mp4, png or svg\n --script <file.json> [{ \"at\": 0, \"set\": { \"load\": 5 } }, { \"at\": 3, \"scenario\": \"Proposed\" }, …]\n or { \"seconds\", \"fps\", \"script\": […] }\n --scenario <label> start from a scenario's values (--set goes on top)\n --set <key=value> a value before recording starts; repeatable (--set load=30 --set pods=6)\n --caption the title, scenario and load above the board, the verdict below it\n --story play the definition's story, a step at a time (with --caption, its text below the board)\n --poster with .png: the board, the verdict, the limits and the scenarios compared\n --seconds <n> length of a video or GIF (default 8, or the script's)\n --fps <n> frames per second (default 30; GIF 15, at most 50)\n --scale <n> pixels per diagram unit (default 1, PNG 2)\n --seed <n> the sim's seed (default 1)\n --name <name> which exposed loadmap, when the page has several (default \"default\")\n --width <px> the browser window (default 1280; below 640 the diagram runs top to bottom)\n --color-scheme <s> light or dark, for a page that follows the system theme (default light)";
|
|
44
44
|
export interface RecordArgs {
|
|
45
45
|
/** A page that calls expose(); empty with `file`. */
|
|
46
46
|
url: string;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { t as USAGE, o as check, a as fmt, s as github, n as main, r as parseRecord, i as record } from "./src-
|
|
1
|
+
export { t as USAGE, o as check, a as fmt, s as github, n as main, r as parseRecord, i as record } from "./src-KVw61wOu.js";
|
|
@@ -2,7 +2,6 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { extname, join, normalize } from "node:path";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { chromium } from "playwright";
|
|
6
5
|
// ../core/src/capacity.ts
|
|
7
6
|
/** A capacity read off a dashboard or a load test. */
|
|
8
7
|
function measured(value, unit, o) {
|
|
@@ -154,6 +153,13 @@ function dateAt(g, multiple) {
|
|
|
154
153
|
const at = Date.UTC(y, mo, Math.min(start.getUTCDate(), days));
|
|
155
154
|
return new Date(at + (months - whole) * days * 864e5);
|
|
156
155
|
}
|
|
156
|
+
// ../core/src/internal.ts
|
|
157
|
+
const blamed = /* @__PURE__ */ new WeakMap();
|
|
158
|
+
/** The flow an error was raised computing: by that flow itself, not by a component or another flow it read. */
|
|
159
|
+
const flowOf = (err) => typeof err === "object" && err !== null ? blamed.get(err) : void 0;
|
|
160
|
+
function blame(err, flow) {
|
|
161
|
+
if (typeof err === "object" && err !== null && !blamed.has(err)) blamed.set(err, flow);
|
|
162
|
+
}
|
|
157
163
|
// ../core/src/params.ts
|
|
158
164
|
const p = {
|
|
159
165
|
/** The load, as a multiple of today. One per model; it drives the limits. */
|
|
@@ -721,14 +727,25 @@ function defineModel(def) {
|
|
|
721
727
|
function context(def, values, bound, pinned, fixed) {
|
|
722
728
|
const cache = /* @__PURE__ */ new Map();
|
|
723
729
|
const stack = [];
|
|
724
|
-
const
|
|
730
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
731
|
+
const memo = (key, f, flow) => {
|
|
725
732
|
if (cache.has(key)) return cache.get(key);
|
|
726
|
-
if (stack.includes(key))
|
|
733
|
+
if (stack.includes(key)) {
|
|
734
|
+
const loop = /* @__PURE__ */ new Error(`loadmap: ${[...stack.slice(stack.indexOf(key)), key].join(" → ")} is a loop`);
|
|
735
|
+
seen.add(loop);
|
|
736
|
+
throw loop;
|
|
737
|
+
}
|
|
727
738
|
stack.push(key);
|
|
728
739
|
try {
|
|
729
740
|
const v = f();
|
|
730
741
|
cache.set(key, v);
|
|
731
742
|
return v;
|
|
743
|
+
} catch (err) {
|
|
744
|
+
if (typeof err === "object" && err !== null && !seen.has(err)) {
|
|
745
|
+
seen.add(err);
|
|
746
|
+
if (flow) blame(err, flow);
|
|
747
|
+
}
|
|
748
|
+
throw err;
|
|
732
749
|
} finally {
|
|
733
750
|
stack.pop();
|
|
734
751
|
}
|
|
@@ -765,11 +782,15 @@ function context(def, values, bound, pinned, fixed) {
|
|
|
765
782
|
if (r.budget != null && !(r.budget >= 0 && Number.isFinite(r.budget))) throw new Error(`loadmap: flow "${f}" needs a retry budget of 0 or more, not ${r.budget}`);
|
|
766
783
|
return r.times > 0 && r.budget !== 0 ? r : void 0;
|
|
767
784
|
};
|
|
768
|
-
const
|
|
785
|
+
const rateOf = (f) => {
|
|
786
|
+
const r = def.flows[f].rate;
|
|
787
|
+
return typeof r === "function" ? r(ctx) : r;
|
|
788
|
+
};
|
|
789
|
+
const rate = (f) => memo(`rate of ${f}`, () => rateOf(f), f);
|
|
769
790
|
ctx.flows = table(flowIds, (f) => memo(`flow ${f}`, () => {
|
|
770
791
|
if (fixed && Object.hasOwn(fixed, f)) return fixed[f];
|
|
771
|
-
return retryOf(f) ? retries()[f].attempts :
|
|
772
|
-
}));
|
|
792
|
+
return retryOf(f) ? retries()[f].attempts : rateOf(f);
|
|
793
|
+
}, f));
|
|
773
794
|
/** The share a stage turns away: past its component's `full`, what is above it. */
|
|
774
795
|
const rejects = (s) => memo(`rejects at ${s}`, () => {
|
|
775
796
|
const st = stages[s];
|
|
@@ -1478,6 +1499,8 @@ const PANELS = [
|
|
|
1478
1499
|
"compare",
|
|
1479
1500
|
"assumptions"
|
|
1480
1501
|
];
|
|
1502
|
+
/** What full screen opens with, as the `fullScreenMode` prop of `<Loadmap>` names it. */
|
|
1503
|
+
const FULL_SCREEN_MODES = ["panels", "focus"];
|
|
1481
1504
|
const COLORS = [
|
|
1482
1505
|
"accent",
|
|
1483
1506
|
"alt",
|
|
@@ -1509,11 +1532,22 @@ const SCHEMA = {
|
|
|
1509
1532
|
scope: "const",
|
|
1510
1533
|
doc: "Simulated seconds per real second. Default 10."
|
|
1511
1534
|
},
|
|
1535
|
+
transition: {
|
|
1536
|
+
value: "number",
|
|
1537
|
+
scope: "const",
|
|
1538
|
+
doc: "Seconds the diagram takes to move to a new structure (a scenario, a story step, a control that adds pods); 0 cuts. Default 0.8, at most 5; with reduced motion it cuts."
|
|
1539
|
+
},
|
|
1512
1540
|
fullScreen: {
|
|
1513
1541
|
value: "names",
|
|
1514
1542
|
scope: "const",
|
|
1515
1543
|
values: PANELS,
|
|
1516
|
-
doc: "The panels
|
|
1544
|
+
doc: "The panels full screen shows, compact, below the diagram. Default all of them."
|
|
1545
|
+
},
|
|
1546
|
+
fullScreenMode: {
|
|
1547
|
+
value: "enum",
|
|
1548
|
+
scope: "const",
|
|
1549
|
+
values: FULL_SCREEN_MODES,
|
|
1550
|
+
doc: "What full screen opens with: the panels, or focus (the diagram, the story, the verdict and the load). Default panels."
|
|
1517
1551
|
},
|
|
1518
1552
|
assume: {
|
|
1519
1553
|
value: "string",
|
|
@@ -2224,11 +2258,25 @@ const STAGES = [
|
|
|
2224
2258
|
/** A formula failed while the model ran: where, and why. */
|
|
2225
2259
|
var FormulaError = class extends Error {
|
|
2226
2260
|
span;
|
|
2227
|
-
|
|
2261
|
+
hint;
|
|
2262
|
+
constructor(span, message, hint) {
|
|
2228
2263
|
super(message);
|
|
2229
2264
|
this.span = span;
|
|
2265
|
+
this.hint = hint;
|
|
2230
2266
|
}
|
|
2231
2267
|
};
|
|
2268
|
+
const ISSUES = "https://github.com/jaim3r0jas/loadmap/issues";
|
|
2269
|
+
/**
|
|
2270
|
+
* A run-time error in the definition's words: core's own (`loadmap: …`) as written; a TypeError or ReferenceError, which
|
|
2271
|
+
* no definition can cause, as a bug in loadmap, its text kept for the report.
|
|
2272
|
+
*/
|
|
2273
|
+
function failure(err, what = "this formula") {
|
|
2274
|
+
if (err instanceof TypeError || err instanceof ReferenceError) return {
|
|
2275
|
+
message: `${what} could not be computed: a bug in loadmap`,
|
|
2276
|
+
hint: `Please report it at ${ISSUES} (${err.name}: ${err.message})`
|
|
2277
|
+
};
|
|
2278
|
+
return { message: err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err) };
|
|
2279
|
+
}
|
|
2232
2280
|
const num = (f) => f;
|
|
2233
2281
|
/** Functions renamed before 0.1: a definition from before says what to write instead. */
|
|
2234
2282
|
const RENAMED = { kfmt: "compact" };
|
|
@@ -2580,11 +2628,21 @@ var Compiler$1 = class {
|
|
|
2580
2628
|
};
|
|
2581
2629
|
}
|
|
2582
2630
|
const host = own(this.names.functions, name);
|
|
2583
|
-
if (host)
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2631
|
+
if (host) {
|
|
2632
|
+
const run = (c, l) => {
|
|
2633
|
+
const args = runs.map((r) => r(c, l));
|
|
2634
|
+
try {
|
|
2635
|
+
return host(...args);
|
|
2636
|
+
} catch (err) {
|
|
2637
|
+
throw new Error(`${name}(…) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
return {
|
|
2641
|
+
type: "any",
|
|
2642
|
+
constant: false,
|
|
2643
|
+
run
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2588
2646
|
const known = [
|
|
2589
2647
|
...Object.keys(BUILTINS),
|
|
2590
2648
|
...this.names.fits.keys(),
|
|
@@ -2672,15 +2730,16 @@ var Compiler$1 = class {
|
|
|
2672
2730
|
};
|
|
2673
2731
|
/**
|
|
2674
2732
|
* Wraps a formula for the model: it takes the context, and any error it throws carries the formula's place. An error
|
|
2675
|
-
* already placed (from a formula this one read) keeps its place: the innermost
|
|
2733
|
+
* already placed (from a formula or a flow this one read) keeps its place: the innermost one is the one to fix.
|
|
2676
2734
|
*/
|
|
2677
2735
|
function placed(span, run) {
|
|
2678
2736
|
return (ctx, local) => {
|
|
2679
2737
|
try {
|
|
2680
2738
|
return run(ctx, local);
|
|
2681
2739
|
} catch (err) {
|
|
2682
|
-
if (err instanceof FormulaError) throw err;
|
|
2683
|
-
|
|
2740
|
+
if (err instanceof FormulaError || flowOf(err)) throw err;
|
|
2741
|
+
const { message, hint } = failure(err);
|
|
2742
|
+
throw new FormulaError(span, message, hint);
|
|
2684
2743
|
}
|
|
2685
2744
|
};
|
|
2686
2745
|
}
|
|
@@ -3098,7 +3157,13 @@ var Parser = class {
|
|
|
3098
3157
|
from: chain[0].from,
|
|
3099
3158
|
to: chain.at(-1).to
|
|
3100
3159
|
};
|
|
3101
|
-
if (this.
|
|
3160
|
+
if (this.is(":") && this.is("{", 1)) {
|
|
3161
|
+
const colon = this.next();
|
|
3162
|
+
e.colon = {
|
|
3163
|
+
from: colon.from,
|
|
3164
|
+
to: colon.to
|
|
3165
|
+
};
|
|
3166
|
+
} else if (this.eat(":")) {
|
|
3102
3167
|
if (this.endsStatement()) this.fail(this.peek(), "expected a value after \":\"");
|
|
3103
3168
|
e.value = this.expr();
|
|
3104
3169
|
e.to = e.value.to;
|
|
@@ -3403,6 +3468,8 @@ function describe(t) {
|
|
|
3403
3468
|
default: return `"${t.text}"`;
|
|
3404
3469
|
}
|
|
3405
3470
|
}
|
|
3471
|
+
/** The longest move to a new structure a definition may ask for, in seconds. */
|
|
3472
|
+
const MAX_TRANSITION = 5;
|
|
3406
3473
|
/** Counts that size the drawing; larger ones would hang the layout, so a shared link cannot ask for them. */
|
|
3407
3474
|
const LIMITS = {
|
|
3408
3475
|
members: 64,
|
|
@@ -3429,7 +3496,10 @@ function compile(source, o = {}) {
|
|
|
3429
3496
|
};
|
|
3430
3497
|
});
|
|
3431
3498
|
} catch (err) {
|
|
3432
|
-
if (err instanceof FormulaError
|
|
3499
|
+
if (err instanceof FormulaError || flowOf(err) || err instanceof Error && err.message.startsWith("loadmap: ")) c.runError(err, {
|
|
3500
|
+
from: 0,
|
|
3501
|
+
to: 0
|
|
3502
|
+
});
|
|
3433
3503
|
else if (!(err instanceof Bail)) report.error({
|
|
3434
3504
|
from: 0,
|
|
3435
3505
|
to: 0
|
|
@@ -3611,6 +3681,7 @@ var Compiler = class {
|
|
|
3611
3681
|
for (const b of flows) {
|
|
3612
3682
|
const id = b.args.id;
|
|
3613
3683
|
if (!id) continue;
|
|
3684
|
+
this.flowAt.set(id.name, id);
|
|
3614
3685
|
const label = this.text(b.args.label);
|
|
3615
3686
|
const def = {};
|
|
3616
3687
|
if (label != null) def.label = label;
|
|
@@ -3659,7 +3730,7 @@ var Compiler = class {
|
|
|
3659
3730
|
}
|
|
3660
3731
|
for (const [id, s] of stageOf) if (!reached.has(id) && s.block.keyword !== "fanout") this.report.warning(s.block.args.id ?? s.block.at, `no flow passes the stage "${id}"`);
|
|
3661
3732
|
const used = new Set(Object.values(stageDefs).map((d) => d.component));
|
|
3662
|
-
for (const [id, c] of Object.entries(componentDefs)) if (!c.demand && !used.has(id)) this.report.error(componentNames.get(id), `the component "${id}" needs "demand: …": no stage uses it`, COMPONENT_PROPS.demand.doc);
|
|
3733
|
+
for (const [id, c] of Object.entries(componentDefs)) if (!c.demand && !used.has(id) && !this.plainBoxes.has(id)) this.report.error(componentNames.get(id), `the component "${id}" needs "demand: …": no stage uses it`, COMPONENT_PROPS.demand.doc);
|
|
3663
3734
|
const calibration = {};
|
|
3664
3735
|
for (const f of fits) {
|
|
3665
3736
|
const fit = f.args.id && this.fits.get(f.args.id.name);
|
|
@@ -3704,10 +3775,15 @@ var Compiler = class {
|
|
|
3704
3775
|
const options = { assumptions: [] };
|
|
3705
3776
|
const speed = this.constProp(root, "speed");
|
|
3706
3777
|
if (speed != null) options.speed = speed;
|
|
3778
|
+
const transition = this.constProp(root, "transition");
|
|
3779
|
+
if (transition != null && (transition < 0 || transition > MAX_TRANSITION)) this.report.error(this.prop(root, "transition").value, `transition is ${transition} seconds; 0 to ${MAX_TRANSITION}`);
|
|
3780
|
+
else if (transition != null) options.transition = transition;
|
|
3707
3781
|
const layout = this.prop(root, "layout");
|
|
3708
3782
|
if (layout) options.direction = this.enumValue(layout, SCHEMA[""].props.layout) === "down" ? "DOWN" : "RIGHT";
|
|
3709
3783
|
const panels = this.prop(root, "fullScreen");
|
|
3710
3784
|
if (panels) options.fullScreen = this.words(panels, SCHEMA[""].props.fullScreen);
|
|
3785
|
+
const mode = this.prop(root, "fullScreenMode");
|
|
3786
|
+
if (mode) options.fullScreenMode = this.enumValue(mode, SCHEMA[""].props.fullScreenMode);
|
|
3711
3787
|
for (const a of root.props.get("assume") ?? []) {
|
|
3712
3788
|
const v = this.constant(a.value, "string");
|
|
3713
3789
|
if (typeof v === "string") options.assumptions.push(v);
|
|
@@ -3731,7 +3807,7 @@ var Compiler = class {
|
|
|
3731
3807
|
model = defineModel(modelDef);
|
|
3732
3808
|
view = defineView(model, viewDef);
|
|
3733
3809
|
} catch (err) {
|
|
3734
|
-
this.
|
|
3810
|
+
this.runError(err, top);
|
|
3735
3811
|
return;
|
|
3736
3812
|
}
|
|
3737
3813
|
const runs = [{
|
|
@@ -3754,7 +3830,7 @@ var Compiler = class {
|
|
|
3754
3830
|
at: b.at
|
|
3755
3831
|
});
|
|
3756
3832
|
}
|
|
3757
|
-
for (const { values, at } of runs) try {
|
|
3833
|
+
for (const [i, { values, at }] of runs.entries()) try {
|
|
3758
3834
|
const ev = model.evaluate(values);
|
|
3759
3835
|
labelsAt(view, ev);
|
|
3760
3836
|
for (const k of Object.keys(modelDef.derived ?? {})) ev.ctx.derived[k];
|
|
@@ -3768,8 +3844,7 @@ var Compiler = class {
|
|
|
3768
3844
|
for (const e of viewDef.edges) e.when?.(ev.ctx);
|
|
3769
3845
|
for (const f of model.flowIds) if (!Number.isFinite(ev.flows[f])) this.report.warning(this.flowRate.get(f) ?? at, `the rate of "${f}" is ${ev.flows[f]} here`);
|
|
3770
3846
|
} catch (err) {
|
|
3771
|
-
|
|
3772
|
-
else this.report.error(at, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
|
|
3847
|
+
this.runError(err, at, i === 0);
|
|
3773
3848
|
}
|
|
3774
3849
|
if (this.report.failed) return void 0;
|
|
3775
3850
|
return {
|
|
@@ -3778,7 +3853,19 @@ var Compiler = class {
|
|
|
3778
3853
|
options
|
|
3779
3854
|
};
|
|
3780
3855
|
}
|
|
3856
|
+
/**
|
|
3857
|
+
* What running the model threw, where it came from: its formula, else its flow, else `at`. A preset or a scenario
|
|
3858
|
+
* keeps `at` (`onFlow` false): the flow is fine at the defaults, the preset is what breaks it.
|
|
3859
|
+
*/
|
|
3860
|
+
runError(err, at, onFlow = true) {
|
|
3861
|
+
if (err instanceof FormulaError) return this.report.error(err.span, err.message, err.hint);
|
|
3862
|
+
const flow = flowOf(err);
|
|
3863
|
+
const { message, hint } = failure(err, flow ? `the flow "${flow}"` : "the model");
|
|
3864
|
+
this.report.error(onFlow && flow && this.flowAt.get(flow) || at, message, hint);
|
|
3865
|
+
}
|
|
3781
3866
|
flowRate = /* @__PURE__ */ new Map();
|
|
3867
|
+
flowAt = /* @__PURE__ */ new Map();
|
|
3868
|
+
plainBoxes = /* @__PURE__ */ new Set();
|
|
3782
3869
|
shape(keyword, spec, stmts, at, decl) {
|
|
3783
3870
|
const b = {
|
|
3784
3871
|
keyword,
|
|
@@ -3914,6 +4001,15 @@ var Compiler = class {
|
|
|
3914
4001
|
component
|
|
3915
4002
|
};
|
|
3916
4003
|
}
|
|
4004
|
+
/** A box with only a component's optional lines (`describe`, `name`, …) was meant as a plain box: one error, the fix. */
|
|
4005
|
+
plainBox(b) {
|
|
4006
|
+
const [k, p] = Object.keys(COMPONENT_PROPS).flatMap((k) => b.props.has(k) ? [[k, this.prop(b, k)]] : []).sort((x, y) => x[1].key.from - y[1].key.from)[0];
|
|
4007
|
+
let hint = "give the box capacity and over lines, or drop it";
|
|
4008
|
+
const sub = b.args.sub;
|
|
4009
|
+
const noSub = (!sub || sub.t === "str" && sub.value === "") && !b.props.has("sub");
|
|
4010
|
+
if (k === "describe" && p.value.t === "str" && noSub) hint = b.args.label ? `for a line under a plain box, put it on the first line: ${b.keyword} ${b.args.id.name} ${this.slice(b.args.label)} ${this.slice(p.value)}` : `for a line under a plain box, write sub: ${this.slice(p.value)}`;
|
|
4011
|
+
this.report.error(p.key, `"${k}" belongs to a component, and this box has no capacity`, hint);
|
|
4012
|
+
}
|
|
3917
4013
|
slice(s) {
|
|
3918
4014
|
return this.source.slice(s.from, s.to);
|
|
3919
4015
|
}
|
|
@@ -3942,7 +4038,8 @@ var Compiler = class {
|
|
|
3942
4038
|
try {
|
|
3943
4039
|
return f.run(void 0);
|
|
3944
4040
|
} catch (err) {
|
|
3945
|
-
|
|
4041
|
+
const { message, hint } = failure(err);
|
|
4042
|
+
this.report.error(e, message, hint);
|
|
3946
4043
|
return;
|
|
3947
4044
|
}
|
|
3948
4045
|
}
|
|
@@ -4343,7 +4440,14 @@ var Compiler = class {
|
|
|
4343
4440
|
o[k] = this.dyn(prop.value, want, "ctx", check);
|
|
4344
4441
|
}
|
|
4345
4442
|
const at = box ? box.args.id ?? box.at : b.at;
|
|
4346
|
-
|
|
4443
|
+
const missing = ["capacity", "over"].filter((k) => !(k in o));
|
|
4444
|
+
const nested = b.children.some((c) => c.keyword === "component");
|
|
4445
|
+
if (b === box && !nested && missing.length === 2 && !("demand" in o)) {
|
|
4446
|
+
this.plainBoxes.add(b.args.id.name);
|
|
4447
|
+
this.plainBox(b);
|
|
4448
|
+
return o;
|
|
4449
|
+
}
|
|
4450
|
+
for (const k of missing) this.report.error(at, `the component needs "${k}: …"`, COMPONENT_PROPS[k].doc);
|
|
4347
4451
|
return o;
|
|
4348
4452
|
}
|
|
4349
4453
|
stage(b, box, components, resolve, flows, reached) {
|
|
@@ -4438,6 +4542,10 @@ var Compiler = class {
|
|
|
4438
4542
|
if (!flows.has(f.name)) this.report.error(f, `unknown flow "${f.name}"`, didYouMean(f.name, flows.keys()));
|
|
4439
4543
|
const stage = resolve(s, "stage");
|
|
4440
4544
|
if (stage) reached.add(stage);
|
|
4545
|
+
if (e.colon) {
|
|
4546
|
+
this.report.error(e.colon, "a branch takes its copies after \":\", not a block", `${f.name} -> ${s.name}: 1`);
|
|
4547
|
+
continue;
|
|
4548
|
+
}
|
|
4441
4549
|
if (!e.value) this.report.error(e, "how many copies per unit?", `${f.name} -> ${s.name}: 1`);
|
|
4442
4550
|
if (e.body) this.report.error(e.bodySpan ?? e, "a branch has no block");
|
|
4443
4551
|
if (stage && e.value && flows.has(f.name)) branches.push([
|
|
@@ -4511,6 +4619,7 @@ var Compiler = class {
|
|
|
4511
4619
|
};
|
|
4512
4620
|
}
|
|
4513
4621
|
edges(e, boxes) {
|
|
4622
|
+
if (e.colon) this.report.error(e.colon, "drop the \":\" before a { … } block", `${e.chain.map((c) => c.name).join(" -> ")} { ${e.chain.length > 2 ? "" : "label: …; "}when: … }`);
|
|
4514
4623
|
for (const n of e.chain) if (!boxes.has(n.name)) this.report.error(n, `unknown node "${n.name}"`, didYouMean(n.name, boxes.keys()));
|
|
4515
4624
|
const props = /* @__PURE__ */ new Map();
|
|
4516
4625
|
for (const s of e.body ?? []) {
|
|
@@ -4898,6 +5007,20 @@ async function readStdin() {
|
|
|
4898
5007
|
for await (const c of process.stdin) chunks.push(c);
|
|
4899
5008
|
return Buffer.concat(chunks).toString("utf8");
|
|
4900
5009
|
}
|
|
5010
|
+
// src/playwright.ts
|
|
5011
|
+
/**
|
|
5012
|
+
* Playwright, loaded only to record: `check`, `fmt` and `--help` run without it, so a CI step that never draws does
|
|
5013
|
+
* not install a browser driver. Missing, it says how to add it.
|
|
5014
|
+
*/
|
|
5015
|
+
async function playwright(load = () => import("playwright")) {
|
|
5016
|
+
try {
|
|
5017
|
+
return await load();
|
|
5018
|
+
} catch (err) {
|
|
5019
|
+
const e = err;
|
|
5020
|
+
if ((e.code === "ERR_MODULE_NOT_FOUND" || e.code === "MODULE_NOT_FOUND") && /['"]playwright['"]/.test(e.message ?? "")) throw new Error("loadmap record needs Playwright: npm i -D playwright, then npx playwright install chromium");
|
|
5021
|
+
throw err;
|
|
5022
|
+
}
|
|
5023
|
+
}
|
|
4901
5024
|
// src/index.ts
|
|
4902
5025
|
const USAGE = `loadmap check <file.loadmap>… [--format text|github]
|
|
4903
5026
|
loadmap fmt <file.loadmap>… [--check] | loadmap fmt -
|
|
@@ -4916,7 +5039,8 @@ operators; numbers, texts and formulas stay as written. A file with a syntax err
|
|
|
4916
5039
|
|
|
4917
5040
|
record opens the page in headless Chromium, finds the loadmap it exposes (expose() from @loadmap/export) and
|
|
4918
5041
|
records it on a fixed clock, so the same options give the same file. A .loadmap file is drawn by the viewer that
|
|
4919
|
-
comes with the command.
|
|
5042
|
+
comes with the command. It needs Playwright next to the command: npm i -D playwright, then npx playwright install
|
|
5043
|
+
chromium (check and fmt run without it).
|
|
4920
5044
|
|
|
4921
5045
|
--out <file> the extension picks the format: gif, webm, mp4, png or svg
|
|
4922
5046
|
--script <file.json> [{ "at": 0, "set": { "load": 5 } }, { "at": 3, "scenario": "Proposed" }, …]
|
|
@@ -5039,7 +5163,8 @@ async function record(a, o = {}) {
|
|
|
5039
5163
|
"png",
|
|
5040
5164
|
"svg"
|
|
5041
5165
|
].includes(format)) throw new Error(`--out must end in .gif, .webm, .mp4, .png or .svg`);
|
|
5042
|
-
|
|
5166
|
+
const { chromium } = await playwright();
|
|
5167
|
+
if (!a.file) return recordPage(a, format, chromium);
|
|
5043
5168
|
const source = await readFile(a.file, "utf8");
|
|
5044
5169
|
const r = compile(source);
|
|
5045
5170
|
if (!r.ok) throw new Error(r.diagnostics.map((d) => formatDiagnostic(d, source, a.file)).join("\n"));
|
|
@@ -5048,7 +5173,7 @@ async function record(a, o = {}) {
|
|
|
5048
5173
|
return await recordPage({
|
|
5049
5174
|
...a,
|
|
5050
5175
|
url: server.url
|
|
5051
|
-
}, format);
|
|
5176
|
+
}, format, chromium);
|
|
5052
5177
|
} finally {
|
|
5053
5178
|
await server.close();
|
|
5054
5179
|
}
|
|
@@ -5085,7 +5210,7 @@ async function serve(source, dir) {
|
|
|
5085
5210
|
close: () => new Promise((done) => server.close(() => done()))
|
|
5086
5211
|
};
|
|
5087
5212
|
}
|
|
5088
|
-
async function recordPage(a, format) {
|
|
5213
|
+
async function recordPage(a, format, chromium) {
|
|
5089
5214
|
const browser = await chromium.launch();
|
|
5090
5215
|
try {
|
|
5091
5216
|
const page = await browser.newPage({
|