@loadmap/cli 0.5.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-BQW6xKGu.js → src-KVw61wOu.js} +133 -28
- 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-BZ9jzcc_.js +0 -287
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];
|
|
@@ -2237,11 +2258,25 @@ const STAGES = [
|
|
|
2237
2258
|
/** A formula failed while the model ran: where, and why. */
|
|
2238
2259
|
var FormulaError = class extends Error {
|
|
2239
2260
|
span;
|
|
2240
|
-
|
|
2261
|
+
hint;
|
|
2262
|
+
constructor(span, message, hint) {
|
|
2241
2263
|
super(message);
|
|
2242
2264
|
this.span = span;
|
|
2265
|
+
this.hint = hint;
|
|
2243
2266
|
}
|
|
2244
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
|
+
}
|
|
2245
2280
|
const num = (f) => f;
|
|
2246
2281
|
/** Functions renamed before 0.1: a definition from before says what to write instead. */
|
|
2247
2282
|
const RENAMED = { kfmt: "compact" };
|
|
@@ -2593,11 +2628,21 @@ var Compiler$1 = class {
|
|
|
2593
2628
|
};
|
|
2594
2629
|
}
|
|
2595
2630
|
const host = own(this.names.functions, name);
|
|
2596
|
-
if (host)
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
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
|
+
}
|
|
2601
2646
|
const known = [
|
|
2602
2647
|
...Object.keys(BUILTINS),
|
|
2603
2648
|
...this.names.fits.keys(),
|
|
@@ -2685,15 +2730,16 @@ var Compiler$1 = class {
|
|
|
2685
2730
|
};
|
|
2686
2731
|
/**
|
|
2687
2732
|
* Wraps a formula for the model: it takes the context, and any error it throws carries the formula's place. An error
|
|
2688
|
-
* 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.
|
|
2689
2734
|
*/
|
|
2690
2735
|
function placed(span, run) {
|
|
2691
2736
|
return (ctx, local) => {
|
|
2692
2737
|
try {
|
|
2693
2738
|
return run(ctx, local);
|
|
2694
2739
|
} catch (err) {
|
|
2695
|
-
if (err instanceof FormulaError) throw err;
|
|
2696
|
-
|
|
2740
|
+
if (err instanceof FormulaError || flowOf(err)) throw err;
|
|
2741
|
+
const { message, hint } = failure(err);
|
|
2742
|
+
throw new FormulaError(span, message, hint);
|
|
2697
2743
|
}
|
|
2698
2744
|
};
|
|
2699
2745
|
}
|
|
@@ -3111,7 +3157,13 @@ var Parser = class {
|
|
|
3111
3157
|
from: chain[0].from,
|
|
3112
3158
|
to: chain.at(-1).to
|
|
3113
3159
|
};
|
|
3114
|
-
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(":")) {
|
|
3115
3167
|
if (this.endsStatement()) this.fail(this.peek(), "expected a value after \":\"");
|
|
3116
3168
|
e.value = this.expr();
|
|
3117
3169
|
e.to = e.value.to;
|
|
@@ -3444,7 +3496,10 @@ function compile(source, o = {}) {
|
|
|
3444
3496
|
};
|
|
3445
3497
|
});
|
|
3446
3498
|
} catch (err) {
|
|
3447
|
-
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
|
+
});
|
|
3448
3503
|
else if (!(err instanceof Bail)) report.error({
|
|
3449
3504
|
from: 0,
|
|
3450
3505
|
to: 0
|
|
@@ -3626,6 +3681,7 @@ var Compiler = class {
|
|
|
3626
3681
|
for (const b of flows) {
|
|
3627
3682
|
const id = b.args.id;
|
|
3628
3683
|
if (!id) continue;
|
|
3684
|
+
this.flowAt.set(id.name, id);
|
|
3629
3685
|
const label = this.text(b.args.label);
|
|
3630
3686
|
const def = {};
|
|
3631
3687
|
if (label != null) def.label = label;
|
|
@@ -3674,7 +3730,7 @@ var Compiler = class {
|
|
|
3674
3730
|
}
|
|
3675
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}"`);
|
|
3676
3732
|
const used = new Set(Object.values(stageDefs).map((d) => d.component));
|
|
3677
|
-
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);
|
|
3678
3734
|
const calibration = {};
|
|
3679
3735
|
for (const f of fits) {
|
|
3680
3736
|
const fit = f.args.id && this.fits.get(f.args.id.name);
|
|
@@ -3751,7 +3807,7 @@ var Compiler = class {
|
|
|
3751
3807
|
model = defineModel(modelDef);
|
|
3752
3808
|
view = defineView(model, viewDef);
|
|
3753
3809
|
} catch (err) {
|
|
3754
|
-
this.
|
|
3810
|
+
this.runError(err, top);
|
|
3755
3811
|
return;
|
|
3756
3812
|
}
|
|
3757
3813
|
const runs = [{
|
|
@@ -3774,7 +3830,7 @@ var Compiler = class {
|
|
|
3774
3830
|
at: b.at
|
|
3775
3831
|
});
|
|
3776
3832
|
}
|
|
3777
|
-
for (const { values, at } of runs) try {
|
|
3833
|
+
for (const [i, { values, at }] of runs.entries()) try {
|
|
3778
3834
|
const ev = model.evaluate(values);
|
|
3779
3835
|
labelsAt(view, ev);
|
|
3780
3836
|
for (const k of Object.keys(modelDef.derived ?? {})) ev.ctx.derived[k];
|
|
@@ -3788,8 +3844,7 @@ var Compiler = class {
|
|
|
3788
3844
|
for (const e of viewDef.edges) e.when?.(ev.ctx);
|
|
3789
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`);
|
|
3790
3846
|
} catch (err) {
|
|
3791
|
-
|
|
3792
|
-
else this.report.error(at, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
|
|
3847
|
+
this.runError(err, at, i === 0);
|
|
3793
3848
|
}
|
|
3794
3849
|
if (this.report.failed) return void 0;
|
|
3795
3850
|
return {
|
|
@@ -3798,7 +3853,19 @@ var Compiler = class {
|
|
|
3798
3853
|
options
|
|
3799
3854
|
};
|
|
3800
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
|
+
}
|
|
3801
3866
|
flowRate = /* @__PURE__ */ new Map();
|
|
3867
|
+
flowAt = /* @__PURE__ */ new Map();
|
|
3868
|
+
plainBoxes = /* @__PURE__ */ new Set();
|
|
3802
3869
|
shape(keyword, spec, stmts, at, decl) {
|
|
3803
3870
|
const b = {
|
|
3804
3871
|
keyword,
|
|
@@ -3934,6 +4001,15 @@ var Compiler = class {
|
|
|
3934
4001
|
component
|
|
3935
4002
|
};
|
|
3936
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
|
+
}
|
|
3937
4013
|
slice(s) {
|
|
3938
4014
|
return this.source.slice(s.from, s.to);
|
|
3939
4015
|
}
|
|
@@ -3962,7 +4038,8 @@ var Compiler = class {
|
|
|
3962
4038
|
try {
|
|
3963
4039
|
return f.run(void 0);
|
|
3964
4040
|
} catch (err) {
|
|
3965
|
-
|
|
4041
|
+
const { message, hint } = failure(err);
|
|
4042
|
+
this.report.error(e, message, hint);
|
|
3966
4043
|
return;
|
|
3967
4044
|
}
|
|
3968
4045
|
}
|
|
@@ -4363,7 +4440,14 @@ var Compiler = class {
|
|
|
4363
4440
|
o[k] = this.dyn(prop.value, want, "ctx", check);
|
|
4364
4441
|
}
|
|
4365
4442
|
const at = box ? box.args.id ?? box.at : b.at;
|
|
4366
|
-
|
|
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);
|
|
4367
4451
|
return o;
|
|
4368
4452
|
}
|
|
4369
4453
|
stage(b, box, components, resolve, flows, reached) {
|
|
@@ -4458,6 +4542,10 @@ var Compiler = class {
|
|
|
4458
4542
|
if (!flows.has(f.name)) this.report.error(f, `unknown flow "${f.name}"`, didYouMean(f.name, flows.keys()));
|
|
4459
4543
|
const stage = resolve(s, "stage");
|
|
4460
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
|
+
}
|
|
4461
4549
|
if (!e.value) this.report.error(e, "how many copies per unit?", `${f.name} -> ${s.name}: 1`);
|
|
4462
4550
|
if (e.body) this.report.error(e.bodySpan ?? e, "a branch has no block");
|
|
4463
4551
|
if (stage && e.value && flows.has(f.name)) branches.push([
|
|
@@ -4531,6 +4619,7 @@ var Compiler = class {
|
|
|
4531
4619
|
};
|
|
4532
4620
|
}
|
|
4533
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: … }`);
|
|
4534
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()));
|
|
4535
4624
|
const props = /* @__PURE__ */ new Map();
|
|
4536
4625
|
for (const s of e.body ?? []) {
|
|
@@ -4918,6 +5007,20 @@ async function readStdin() {
|
|
|
4918
5007
|
for await (const c of process.stdin) chunks.push(c);
|
|
4919
5008
|
return Buffer.concat(chunks).toString("utf8");
|
|
4920
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
|
+
}
|
|
4921
5024
|
// src/index.ts
|
|
4922
5025
|
const USAGE = `loadmap check <file.loadmap>… [--format text|github]
|
|
4923
5026
|
loadmap fmt <file.loadmap>… [--check] | loadmap fmt -
|
|
@@ -4936,7 +5039,8 @@ operators; numbers, texts and formulas stay as written. A file with a syntax err
|
|
|
4936
5039
|
|
|
4937
5040
|
record opens the page in headless Chromium, finds the loadmap it exposes (expose() from @loadmap/export) and
|
|
4938
5041
|
records it on a fixed clock, so the same options give the same file. A .loadmap file is drawn by the viewer that
|
|
4939
|
-
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).
|
|
4940
5044
|
|
|
4941
5045
|
--out <file> the extension picks the format: gif, webm, mp4, png or svg
|
|
4942
5046
|
--script <file.json> [{ "at": 0, "set": { "load": 5 } }, { "at": 3, "scenario": "Proposed" }, …]
|
|
@@ -5059,7 +5163,8 @@ async function record(a, o = {}) {
|
|
|
5059
5163
|
"png",
|
|
5060
5164
|
"svg"
|
|
5061
5165
|
].includes(format)) throw new Error(`--out must end in .gif, .webm, .mp4, .png or .svg`);
|
|
5062
|
-
|
|
5166
|
+
const { chromium } = await playwright();
|
|
5167
|
+
if (!a.file) return recordPage(a, format, chromium);
|
|
5063
5168
|
const source = await readFile(a.file, "utf8");
|
|
5064
5169
|
const r = compile(source);
|
|
5065
5170
|
if (!r.ok) throw new Error(r.diagnostics.map((d) => formatDiagnostic(d, source, a.file)).join("\n"));
|
|
@@ -5068,7 +5173,7 @@ async function record(a, o = {}) {
|
|
|
5068
5173
|
return await recordPage({
|
|
5069
5174
|
...a,
|
|
5070
5175
|
url: server.url
|
|
5071
|
-
}, format);
|
|
5176
|
+
}, format, chromium);
|
|
5072
5177
|
} finally {
|
|
5073
5178
|
await server.close();
|
|
5074
5179
|
}
|
|
@@ -5105,7 +5210,7 @@ async function serve(source, dir) {
|
|
|
5105
5210
|
close: () => new Promise((done) => server.close(() => done()))
|
|
5106
5211
|
};
|
|
5107
5212
|
}
|
|
5108
|
-
async function recordPage(a, format) {
|
|
5213
|
+
async function recordPage(a, format, chromium) {
|
|
5109
5214
|
const browser = await chromium.launch();
|
|
5110
5215
|
try {
|
|
5111
5216
|
const page = await browser.newPage({
|