@loadmap/cli 0.5.0 → 0.7.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 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
- npx @loadmap/cli record shop.loadmap --out shop.gif --caption
7
- npx @loadmap/cli record http://localhost:5173/ --out poster.png --poster
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. Recording needs Chromium
15
- once: `npx playwright install chromium`. `loadmap --help` lists every option.
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as main } from "./src-BQW6xKGu.js";
2
+ import { n as main } from "./src-KVw61wOu.js";
3
3
  // src/bin.ts
4
4
  main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
5
5
  console.error(err instanceof Error ? err.message : err);
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-BQW6xKGu.js";
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 memo = (key, f) => {
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)) throw new Error(`loadmap: ${[...stack.slice(stack.indexOf(key)), key].join(" → ")} is a loop`);
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 rate = (f) => memo(`rate of ${f}`, () => def.flows[f].rate(ctx));
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 : def.flows[f].rate(ctx);
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
- constructor(span, message) {
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) return {
2597
- type: "any",
2598
- constant: false,
2599
- run: (c, l) => host(...runs.map((r) => r(c, l)))
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 formula is the one to fix.
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
- throw new FormulaError(span, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
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.eat(":")) {
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) report.error(err.span, err.message);
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.report.error(top, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
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
- if (err instanceof FormulaError) this.report.error(err.span, err.message);
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
- this.report.error(e, err instanceof Error ? err.message : String(err));
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
- for (const k of ["capacity", "over"]) if (!(k in o)) this.report.error(at, `the component needs "${k}: …"`, COMPONENT_PROPS[k].doc);
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
- if (!a.file) return recordPage(a, format);
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({