@mantaq/inspect 0.4.0 → 0.5.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
@@ -22,6 +22,8 @@ export default defineConfig({
22
22
 
23
23
  The live page runs the real actor, so it needs an exported factory on a `VirtualClock`; instance or `RealClock` machines fall back to the static walk.
24
24
 
25
+ `vite build` with the plugin also writes the page as a static site: `index.html`, `viz.json` and a script under `base` (set `base: "/"` to put them at the output root). Machines load through your Vite config, and the fuzz runs at build time.
26
+
25
27
  Options: `base` (default `/__mantaq/`), `fuzz` (`false`, or `{ seed, iterations }`; default 500 iterations so a save redraws fast).
26
28
 
27
29
  **Library**: one call per machine.
@@ -34,12 +36,14 @@ const { machine, warnings } = inspect({
34
36
  id: "cart",
35
37
  create: () => createCart(), // a fresh actor every call
36
38
  source: { file: "/abs/src/cart.ts", root: "/abs" }, // optional: explanations + payloads from types
37
- tests: [[{ kind: "event", event: "addItem", payload: { sku: "A1" } }]], // optional: "example tests" coverage
39
+ tests: [
40
+ { name: "add an item", steps: [{ kind: "event", event: "addItem", payload: { sku: "A1" } }] },
41
+ ], // optional: named runs for "example tests" coverage
38
42
  });
39
43
  const json = serializeViz(vizDocument([machine]));
40
44
  ```
41
45
 
42
- `inspectFile(file)` / `inspectModule(file, { load })` do the discovery the Vite plugin does. The building blocks (`toVizMachine`, `annotateMachine`, `fuzz`, `fuzzedMachine`, `withTestCoverage`, `guidance`) stay exported for custom pipelines.
46
+ `inspectFile(file)` / `inspectModule(file, { load })` do the discovery the Vite plugin does, and read recorded runs from `__runs__/*.json` next to the machine file (`{ [machineId]: { name, end, steps }[] }`). A run is replayed on a fresh actor; internal events the replay already produces (timers, synchronous emits) are dropped, and a run that replays to a different state than `end` is dropped with a warning. The building blocks (`toVizMachine`, `annotateMachine`, `fuzz`, `fuzzedMachine`, `withTestCoverage`, `guidance`) stay exported for custom pipelines.
43
47
 
44
48
  ## What it needs from your file
45
49
 
@@ -56,7 +60,7 @@ Every part that cannot run is skipped with a warning, never guessed.
56
60
  The viewer's "Coverage from" picker dims elements with zero hits. What dimmed means depends on the source:
57
61
 
58
62
  - **examples + fuzz** (`reached`): neither an example nor fuzzing reached this. The default when both sources are present, and the same merge `expectReached` uses. Fix a miss by adding an example to `tests`.
59
- - **example tests** (`tested`): no test exercises this. Collected by replaying the event sequences you pass as `tests`, each on a fresh actor.
63
+ - **example tests** (`tested`): no test exercises this. Collected by replaying the named runs you pass as `tests`, each on a fresh actor.
60
64
  - **fuzz runs** (`coverage`): no fuzz run produced this. Fuzzing reaches almost everything, so little is dimmed. What does stay dimmed is usually a **declared but unreached edge**: the handler lists the target in `to`, but its guards may never allow it. Saga's `compensatingRefund -REFUND_DONE-> failed` is an example. The edge panel calls these out. Fuzzing is not proof, so check the handler.
61
65
 
62
66
  ## How each tool works
@@ -71,6 +75,18 @@ The viewer's "Coverage from" picker dims elements with zero hits. What dimmed me
71
75
 
72
76
  **Guidance** (`guidance`, Phase 5): literal seeding plus a branch-distance guide built from AST comparisons on payload and context. It helps when handlers compare payload fields against literals far from the samples, or count in context toward a threshold. See `docs/atlas/phase5-comparison.md`.
73
77
 
78
+ ## Record runs from your tests
79
+
80
+ In a vitest file, wrap the factory once and your tests become runs:
81
+
82
+ ```ts
83
+ import { recordRuns } from "@mantaq/inspect/record";
84
+
85
+ const make = recordRuns(createInviteLinkActor);
86
+ ```
87
+
88
+ Every passing test that builds an actor with `make` writes its events and clock moves to `__runs__/<test file>.json` next to the test, keyed by machine id. `inspectModule` and the Vite plugin read those files, so each test shows up in the live page's Runs list and counts as an example run. Run the whole test file to refresh it. With `CI` set, a run that differs from the committed file fails the test, like a stale snapshot. The factory may be called in the test or in `beforeEach`; a retried test is recorded once. Runs with a payload that does not survive JSON (functions, `bigint`, `Map`, class instances) are skipped with a warning. `vitest` is an optional peer dependency.
89
+
74
90
  ## What it cannot show
75
91
 
76
92
  - **Guards** are not evaluated. A declared edge says "the handler may return this", not "some input reaches it". Fuzz coverage and the AST's branch conditions are the evidence; neither is proof.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as eventCatalog, i as Trace, n as Observation, o as execute, r as RunError, t as FuzzTarget } from "./run-BLt0mwXe.mjs";
2
- import { a as ModuleOptions, c as machineId, i as InspectedModule, l as vizDocument, n as InspectTarget, o as inspect, r as Inspected, s as inspectModule, t as FuzzSettings, u as AnalysisSource } from "./inspect-DvQtAcwv.mjs";
2
+ import { a as ModuleOptions, c as machineId, i as InspectedModule, l as vizDocument, n as InspectTarget, o as inspect, r as Inspected, s as inspectModule, t as FuzzSettings, u as AnalysisSource } from "./inspect-vSdt2qtL.mjs";
3
3
  import { VizComparison, VizDocument, VizExplanation, VizFuzzError, VizJson, VizMachine, VizStep } from "@mantaq/viz";
4
4
  import { History } from "@mantaq/traversal";
5
5
  import { Either } from "@mantaq/utils";
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import { S as toVizMachine, _ as fuzzedMachine, a as inspect, b as shrink, d as analyzeSource, f as annotateMachine, g as literalPool, h as guidance, l as vizDocument, m as branchDistanceGuide, o as inspectModule, p as branchDistance, r as inspectFile, s as machineId, u as analyzeModule, v as withTestCoverage, x as toViz, y as fuzz } from "./load-D0_Jl6Wr.mjs";
1
+ import { _ as toViz, a as vizDocument, c as annotateMachine, d as guidance, f as literalPool, g as shrink, h as fuzz, l as branchDistance, m as withTestCoverage, n as inspectModule, o as analyzeModule, p as fuzzedMachine, r as machineId, s as analyzeSource, t as inspect, u as branchDistanceGuide, v as toVizMachine } from "./inspect-B7ZQCNM2.mjs";
2
2
  import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
3
+ import { r as inspectFile } from "./load-CWubcSZU.mjs";
3
4
  import { coveredByAny, unhandledSends } from "@mantaq/viz";
4
5
  // src/gate.ts
5
6
  /**
@@ -79,7 +80,7 @@ function missedReach(machine, expected) {
79
80
  }
80
81
  function failOnMiss(machine, misses) {
81
82
  if (misses.length === 0) return;
82
- throw new Error(`${machine.id} missed ${misses.length} expected ${misses.length === 1 ? "reach" : "reaches"}:\n${misses.map((each) => ` - ${each}`).join("\n")}\nAdd an example run to \`tests\`, or raise \`fuzz.iterations\`, to cover it.`);
83
+ throw new Error(`${machine.id} missed ${misses.length} expected ${misses.length === 1 ? "reach" : "reaches"}:\n${misses.map((each) => ` - ${each}`).join("\n")}\nAdd a named run to \`tests\`, or raise \`fuzz.iterations\`, to cover it.`);
83
84
  }
84
85
  /**
85
86
  * Call from a test: throws unless fuzzing or the target's `tests` visited every
@@ -1,14 +1,12 @@
1
- import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
1
+ import { i as execute, r as eventCatalog, t as applyStep } from "./run-B5huYj_r.mjs";
2
2
  import { ANY_STATE, VIZ_SCHEMA_VERSION, coveredByAny, isAnyState, routingWarnings, unhandledSends } from "@mantaq/viz";
3
3
  import { INITIAL_NODE_ID, buildGraph, parseInternalEventIds } from "@mantaq/traversal";
4
4
  import { Either } from "@mantaq/utils";
5
5
  import ts from "typescript";
6
- import { dirname, join, relative, resolve, sep } from "node:path";
6
+ import { basename, dirname, join, relative, sep } from "node:path";
7
7
  import { DEFAULT_SEED } from "@mantaq/pbt";
8
8
  import { VirtualClock } from "@mantaq/core";
9
- import { existsSync } from "node:fs";
10
- import { fileURLToPath } from "node:url";
11
- import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
9
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
12
10
  // src/structure.ts
13
11
  function byId$1(left, right) {
14
12
  if (left.id < right.id) return -1;
@@ -126,7 +124,8 @@ function toVizMachine(input) {
126
124
  states,
127
125
  events,
128
126
  edges,
129
- fuzz: null
127
+ fuzz: null,
128
+ runs: []
130
129
  };
131
130
  }
132
131
  function toViz(inputs) {
@@ -1557,6 +1556,107 @@ function annotateWith(machine, context) {
1557
1556
  edges
1558
1557
  };
1559
1558
  }
1559
+ // src/runs.ts
1560
+ /** Name of the folder, next to the test files, that holds recorded runs. */
1561
+ const RUNS_DIR = "__runs__";
1562
+ function dropOne(list, item) {
1563
+ const at = list.indexOf(item);
1564
+ if (at < 0) return false;
1565
+ list.splice(at, 1);
1566
+ return true;
1567
+ }
1568
+ /**
1569
+ * Replay a raw recording on a fresh actor and keep only the steps that cannot
1570
+ * be reproduced: an internal event the replay already produced (a timer after
1571
+ * its advance, a synchronous emit after its send) is dropped, one it did not
1572
+ * (a promise result) is kept. Left when the replay ends elsewhere than the
1573
+ * test did.
1574
+ */
1575
+ function normalizeRun(target, run) {
1576
+ const { actor, clock } = target.create();
1577
+ const internal = new Set(eventCatalog(actor).internal);
1578
+ let produced = [];
1579
+ actor.on("transition", { fn: ({ event }) => void produced.push(event.type) });
1580
+ const known = /* @__PURE__ */ new Set([...eventCatalog(actor).inputs, ...internal]);
1581
+ const unknown = run.steps.find((step) => step.kind === "event" && !known.has(step.event));
1582
+ if (unknown?.kind === "event") {
1583
+ actor.dispose();
1584
+ return Either.left(`unknown event "${unknown.event}"`);
1585
+ }
1586
+ const kept = [];
1587
+ for (const step of run.steps) {
1588
+ if (step.kind === "event" && internal.has(step.event) && dropOne(produced, step.event)) continue;
1589
+ produced = [];
1590
+ applyStep(actor, {
1591
+ step,
1592
+ clock,
1593
+ internal
1594
+ });
1595
+ if (step.kind === "event" && internal.has(step.event)) dropOne(produced, step.event);
1596
+ kept.push(step);
1597
+ }
1598
+ const ended = actor.state.name;
1599
+ actor.dispose();
1600
+ return ended === run.end ? Either.right(kept) : Either.left(`replays to ${ended}, the test ended in ${run.end}`);
1601
+ }
1602
+ function isRecord(value) {
1603
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1604
+ }
1605
+ function isJson(value) {
1606
+ return Either.isRight(Either.from(() => JSON.stringify(value)));
1607
+ }
1608
+ function isStep(value) {
1609
+ if (!isRecord(value)) return false;
1610
+ if (value["kind"] === "advance") return typeof value["ms"] === "number" && value["ms"] >= 0;
1611
+ return value["kind"] === "event" && typeof value["event"] === "string" && isJson(value["payload"]);
1612
+ }
1613
+ function isRecorded(value) {
1614
+ return isRecord(value) && typeof value["name"] === "string" && typeof value["end"] === "string" && Array.isArray(value["steps"]) && value["steps"].every(isStep);
1615
+ }
1616
+ function readFile(file) {
1617
+ const name = basename(file);
1618
+ const text = Either.from(() => JSON.parse(readFileSync(file, "utf8")));
1619
+ if (Either.isLeft(text) || !isRecord(text[1])) return {
1620
+ runs: {},
1621
+ warnings: [`${RUNS_DIR}/${name}: not a JSON object, ignored`]
1622
+ };
1623
+ const entries = Object.entries(text[1]).map(([id, runs]) => {
1624
+ const list = Array.isArray(runs) ? runs : [];
1625
+ const valid = list.filter(isRecorded);
1626
+ return {
1627
+ id,
1628
+ valid,
1629
+ bad: Array.isArray(runs) ? list.length - valid.length : 1
1630
+ };
1631
+ });
1632
+ return {
1633
+ runs: Object.fromEntries(entries.map(({ id, valid }) => [id, valid])),
1634
+ warnings: entries.filter(({ bad }) => bad > 0).map(({ id, bad }) => `${RUNS_DIR}/${name}: ${bad} malformed run(s) for "${id}" ignored`)
1635
+ };
1636
+ }
1637
+ /**
1638
+ * Every run recorded for the test files in `dir`: the `__runs__/*.json` files
1639
+ * next to them, merged by machine id in file-name order, and a warning for
1640
+ * each file or run that could not be read.
1641
+ */
1642
+ function readRuns(dir) {
1643
+ const listing = Either.from(() => readdirSync(join(dir, RUNS_DIR)).sort());
1644
+ if (Either.isLeft(listing)) return {
1645
+ runs: {},
1646
+ warnings: []
1647
+ };
1648
+ const merged = {};
1649
+ const warnings = [];
1650
+ for (const name of listing[1].filter((each) => each.endsWith(".json"))) {
1651
+ const read = readFile(join(dir, RUNS_DIR, name));
1652
+ warnings.push(...read.warnings);
1653
+ for (const [id, runs] of Object.entries(read.runs)) merged[id] = [...merged[id] ?? [], ...runs];
1654
+ }
1655
+ return {
1656
+ runs: merged,
1657
+ warnings
1658
+ };
1659
+ }
1560
1660
  // src/inspect.ts
1561
1661
  const DEFAULT_ITERATIONS = 2e3;
1562
1662
  function isVirtualClock(clock) {
@@ -1601,11 +1701,11 @@ function driven(target, context) {
1601
1701
  const fuzzed = settings === false ? structure : fuzzedMachine(run[1], fuzz(run[1], {
1602
1702
  seed: settings?.seed ?? DEFAULT_SEED,
1603
1703
  iterations: settings?.iterations ?? DEFAULT_ITERATIONS,
1604
- ...target.tests === void 0 ? {} : { initial: target.tests },
1704
+ ...target.tests === void 0 ? {} : { initial: target.tests.map((run) => run.steps) },
1605
1705
  ...guidance(structure)
1606
1706
  }));
1607
1707
  return {
1608
- machine: target.tests === void 0 ? fuzzed : withTestCoverage(run[1], fuzzed, target.tests),
1708
+ machine: target.tests === void 0 ? fuzzed : withTestCoverage(run[1], fuzzed, target.tests.map((each) => each.steps)),
1609
1709
  skipped: []
1610
1710
  };
1611
1711
  }
@@ -1624,7 +1724,10 @@ function inspectWith(target, analysis) {
1624
1724
  });
1625
1725
  const quiet = target.fuzz === false && target.tests === void 0;
1626
1726
  const unanalyzed = analysis === null ? [] : [Either.getLeft(analysis)].filter((each) => each !== void 0);
1627
- const annotated = annotate(machine);
1727
+ const annotated = annotate({
1728
+ ...machine,
1729
+ runs: [...target.tests ?? []]
1730
+ });
1628
1731
  return {
1629
1732
  machine: annotated,
1630
1733
  warnings: [
@@ -1678,6 +1781,33 @@ function actorFactory(found, value) {
1678
1781
  return isActor(actor) ? actor : sample;
1679
1782
  };
1680
1783
  }
1784
+ /** The recorded runs of one machine that replay faithfully, and a warning for each that does not. */
1785
+ function replayedRuns(id, create, recorded) {
1786
+ const found = recorded[id] ?? [];
1787
+ if (found.length === 0) return {
1788
+ runs: [],
1789
+ warnings: []
1790
+ };
1791
+ const run = runner({
1792
+ id,
1793
+ create
1794
+ }, {});
1795
+ if (Either.isLeft(run)) return {
1796
+ runs: [],
1797
+ warnings: [`runs not shown: ${run[0]}`]
1798
+ };
1799
+ const results = found.map((each) => ({
1800
+ each,
1801
+ result: normalizeRun(run[1], each)
1802
+ }));
1803
+ return {
1804
+ runs: results.flatMap(({ each, result }) => Either.isRight(result) ? [{
1805
+ name: each.name,
1806
+ steps: result[1]
1807
+ }] : []),
1808
+ warnings: results.flatMap(({ each, result }) => Either.isLeft(result) ? [`run "${each.name}" dropped: ${result[0]}`] : [])
1809
+ };
1810
+ }
1681
1811
  /**
1682
1812
  * Every machine a file exports, found from its types: actors, and functions
1683
1813
  * that build one (optionally taking a clock or `{ clock }`). When a file has
@@ -1703,6 +1833,7 @@ async function inspectModule(file, options) {
1703
1833
  warnings: [`${file}: no exported actor or actor factory found`]
1704
1834
  };
1705
1835
  const exports = await options.load(file);
1836
+ const { runs: recorded, warnings: runWarnings } = readRuns(dirname(file));
1706
1837
  const results = chosen.map((each) => {
1707
1838
  const id = machineId(each.name);
1708
1839
  const create = actorFactory(each, exports[each.name]);
@@ -1711,21 +1842,30 @@ async function inspectModule(file, options) {
1711
1842
  warnings: [`${id}: ${each.name} did not return an actor`]
1712
1843
  };
1713
1844
  const fuzzSettings = each.call === "instance" ? false : options.fuzz ?? {};
1845
+ const replayed = each.call === "instance" ? {
1846
+ runs: [],
1847
+ warnings: (recorded[id] ?? []).length === 0 ? [] : ["runs not shown: exported as one actor, so they cannot be replayed safely"]
1848
+ } : replayedRuns(id, create, recorded);
1714
1849
  const inspected = inspectWith({
1715
1850
  id,
1716
1851
  create,
1717
1852
  source,
1718
- fuzz: fuzzSettings
1853
+ fuzz: fuzzSettings,
1854
+ ...replayed.runs.length === 0 ? {} : { tests: replayed.runs }
1719
1855
  }, analysis);
1720
1856
  const instance = each.call === "instance" ? ["not fuzzed: exported as one actor, not a factory"] : [];
1721
1857
  return {
1722
1858
  machine: inspected.machine,
1723
- warnings: [...instance, ...inspected.warnings].map((warning) => `${id}: ${warning}`)
1859
+ warnings: [
1860
+ ...instance,
1861
+ ...replayed.warnings,
1862
+ ...inspected.warnings
1863
+ ].map((warning) => `${id}: ${warning}`)
1724
1864
  };
1725
1865
  });
1726
1866
  return {
1727
1867
  machines: results.flatMap((result) => result.machine === null ? [] : [result.machine]),
1728
- warnings: results.flatMap((result) => result.warnings)
1868
+ warnings: [...runWarnings.map((warning) => `${file}: ${warning}`), ...results.flatMap((result) => result.warnings)]
1729
1869
  };
1730
1870
  }
1731
1871
  /** Machines as a document, ready for `serializeViz`. */
@@ -1735,129 +1875,4 @@ function vizDocument(machines) {
1735
1875
  machines: [...machines]
1736
1876
  };
1737
1877
  }
1738
- // src/load.ts
1739
- const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
1740
- const ENTRY = "virtual:mantaq-inspect";
1741
- const RESOLVED = `\0${ENTRY}`;
1742
- /**
1743
- * A module that imports the machine file and the inspector side by side. Run
1744
- * through one module graph, both see the same `@mantaq/core` instance: core
1745
- * recognizes its state refs by a module-level registry, so a machine loaded
1746
- * through a bundler and inspected from plain Node would look stateless.
1747
- */
1748
- function entrySource(file) {
1749
- const inspect = fileURLToPath(import.meta.resolve("@mantaq/inspect"));
1750
- return [
1751
- `import * as machines from ${JSON.stringify(file)};`,
1752
- `import { inspectModule } from ${JSON.stringify(inspect)};`,
1753
- `export function run(options) {`,
1754
- ` return inspectModule(${JSON.stringify(file)}, { ...options, load: async () => machines });`,
1755
- `}`
1756
- ].join("\n");
1757
- }
1758
- /** Vite plugin serving the entry for each `virtual:mantaq-inspect?file=<path>` id. */
1759
- function inspectEntry() {
1760
- return {
1761
- name: "mantaq-inspect-entry",
1762
- resolveId: (id) => id.startsWith(ENTRY) ? `\0${id}` : null,
1763
- load: (id) => {
1764
- if (!id.startsWith(RESOLVED)) return null;
1765
- const file = new URLSearchParams(id.slice(id.indexOf("?") + 1)).get("file");
1766
- return file === null ? null : entrySource(file);
1767
- }
1768
- };
1769
- }
1770
- function entryId(file) {
1771
- return `${ENTRY}?${new URLSearchParams({ file }).toString()}`;
1772
- }
1773
- /** The `run` export of an entry module, checked: module objects are untyped. */
1774
- function runOf(module) {
1775
- const run = typeof module === "object" && module !== null ? Reflect.get(module, "run") : void 0;
1776
- if (typeof run !== "function") return null;
1777
- return async (options) => Reflect.apply(run, void 0, [options]);
1778
- }
1779
- const ENV_ACCESS = [
1780
- "import",
1781
- "meta",
1782
- "env"
1783
- ].join(".");
1784
- /**
1785
- * Replaces the env object with the project's env: `runnerImport` never reads
1786
- * env files, so `VITE_*` variables would otherwise be missing. A textual
1787
- * replace: it also rewrites matches in strings and comments, and a frozen
1788
- * snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
1789
- */
1790
- function projectEnv(env, mode) {
1791
- const value = JSON.stringify({
1792
- ...env,
1793
- BASE_URL: "/",
1794
- MODE: mode,
1795
- DEV: true,
1796
- PROD: false,
1797
- SSR: true
1798
- });
1799
- return {
1800
- name: "mantaq-inspect-env",
1801
- enforce: "pre",
1802
- transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
1803
- code: code.replaceAll(ENV_ACCESS, `(${value})`),
1804
- map: null
1805
- }
1806
- };
1807
- }
1808
- /**
1809
- * The inline config for a load: empty without `--config`; otherwise the
1810
- * project's Vite config (plugins, aliases, define) plus its env files.
1811
- */
1812
- async function inlineConfig({ config, mode }) {
1813
- const base = {
1814
- configFile: false,
1815
- logLevel: "silent"
1816
- };
1817
- if (config === void 0) return {
1818
- ...base,
1819
- ...mode === void 0 ? {} : { mode }
1820
- };
1821
- const chosen = mode ?? "development";
1822
- const user = (await loadConfigFromFile({
1823
- command: "serve",
1824
- mode: chosen
1825
- }, config))?.config ?? {};
1826
- const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
1827
- const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
1828
- return {
1829
- ...user,
1830
- ...base,
1831
- mode: chosen,
1832
- plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
1833
- };
1834
- }
1835
- /** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
1836
- async function inspectFile(file, options = {}, load = {}) {
1837
- if (load.config !== void 0 && !existsSync(load.config)) return {
1838
- machines: [],
1839
- warnings: [`${file}: Vite config not found: ${load.config}`],
1840
- failed: true
1841
- };
1842
- const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
1843
- ...inline,
1844
- plugins: [...inline.plugins ?? [], inspectEntry()]
1845
- })).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
1846
- if (Either.isLeft(loaded)) return {
1847
- machines: [],
1848
- warnings: [`${file}: ${loaded[0]}`],
1849
- failed: true
1850
- };
1851
- const { module } = loaded[1];
1852
- const run = runOf(module);
1853
- if (run === null) return {
1854
- machines: [],
1855
- warnings: [`${file}: could not load the inspector`],
1856
- failed: true
1857
- };
1858
- return {
1859
- ...await run(options),
1860
- failed: false
1861
- };
1862
- }
1863
- export { toVizMachine as S, fuzzedMachine as _, inspect as a, shrink as b, projectRoot as c, analyzeSource as d, annotateMachine as f, literalPool as g, guidance as h, runOf as i, vizDocument as l, branchDistanceGuide as m, inspectEntry as n, inspectModule as o, branchDistance as p, inspectFile as r, machineId as s, entryId as t, analyzeModule as u, withTestCoverage as v, toViz as x, fuzz as y };
1878
+ export { toViz as _, vizDocument as a, annotateMachine as c, guidance as d, literalPool as f, shrink as g, fuzz as h, projectRoot as i, branchDistance as l, withTestCoverage as m, inspectModule as n, analyzeModule as o, fuzzedMachine as p, machineId as r, analyzeSource as s, inspect as t, branchDistanceGuide as u, toVizMachine as v };
@@ -1,4 +1,4 @@
1
- import { VizDocument, VizJson, VizMachine, VizStep } from "@mantaq/viz";
1
+ import { VizDocument, VizJson, VizMachine, VizRun } from "@mantaq/viz";
2
2
  import { Either } from "@mantaq/utils";
3
3
  import ts from "typescript";
4
4
  import { AnyActor } from "@mantaq/core";
@@ -23,8 +23,8 @@ interface InspectTarget {
23
23
  source?: AnalysisSource;
24
24
  /** Sample payloads per event; replace the ones read from types. */
25
25
  payloads?: Readonly<Record<string, ReadonlyArray<VizJson>>>;
26
- /** Event sequences your tests send, for "Coverage from: example tests". */
27
- tests?: ReadonlyArray<ReadonlyArray<VizStep>>;
26
+ /** Named runs your tests take: "Coverage from: example tests", fuzz seeds, and the live page's Runs list. */
27
+ tests?: ReadonlyArray<VizRun>;
28
28
  /** `false` = structure only. */
29
29
  fuzz?: false | FuzzSettings;
30
30
  }
@@ -0,0 +1,131 @@
1
+ import { Either } from "@mantaq/utils";
2
+ import { dirname, resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
6
+ // src/load.ts
7
+ const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8
+ const ENTRY = "virtual:mantaq-inspect";
9
+ const RESOLVED = `\0${ENTRY}`;
10
+ /**
11
+ * A module that imports the machine file and the inspector side by side. Run
12
+ * through one module graph, both see the same `@mantaq/core` instance: core
13
+ * recognizes its state refs by a module-level registry, so a machine loaded
14
+ * through a bundler and inspected from plain Node would look stateless.
15
+ */
16
+ function entrySource(file) {
17
+ const inspect = fileURLToPath(import.meta.resolve("@mantaq/inspect"));
18
+ return [
19
+ `import * as machines from ${JSON.stringify(file)};`,
20
+ `import { inspectModule } from ${JSON.stringify(inspect)};`,
21
+ `export function run(options) {`,
22
+ ` return inspectModule(${JSON.stringify(file)}, { ...options, load: async () => machines });`,
23
+ `}`
24
+ ].join("\n");
25
+ }
26
+ /** Vite plugin serving the entry for each `virtual:mantaq-inspect?file=<path>` id. */
27
+ function inspectEntry() {
28
+ return {
29
+ name: "mantaq-inspect-entry",
30
+ resolveId: (id) => id.startsWith(ENTRY) ? `\0${id}` : null,
31
+ load: (id) => {
32
+ if (!id.startsWith(RESOLVED)) return null;
33
+ const file = new URLSearchParams(id.slice(id.indexOf("?") + 1)).get("file");
34
+ return file === null ? null : entrySource(file);
35
+ }
36
+ };
37
+ }
38
+ function entryId(file) {
39
+ return `${ENTRY}?${new URLSearchParams({ file }).toString()}`;
40
+ }
41
+ /** The `run` export of an entry module, checked: module objects are untyped. */
42
+ function runOf(module) {
43
+ const run = typeof module === "object" && module !== null ? Reflect.get(module, "run") : void 0;
44
+ if (typeof run !== "function") return null;
45
+ return async (options) => Reflect.apply(run, void 0, [options]);
46
+ }
47
+ const ENV_ACCESS = [
48
+ "import",
49
+ "meta",
50
+ "env"
51
+ ].join(".");
52
+ /**
53
+ * Replaces the env object with the project's env: `runnerImport` never reads
54
+ * env files, so `VITE_*` variables would otherwise be missing. A textual
55
+ * replace: it also rewrites matches in strings and comments, and a frozen
56
+ * snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
57
+ */
58
+ function projectEnv(env, mode) {
59
+ const value = JSON.stringify({
60
+ ...env,
61
+ BASE_URL: "/",
62
+ MODE: mode,
63
+ DEV: true,
64
+ PROD: false,
65
+ SSR: true
66
+ });
67
+ return {
68
+ name: "mantaq-inspect-env",
69
+ enforce: "pre",
70
+ transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
71
+ code: code.replaceAll(ENV_ACCESS, `(${value})`),
72
+ map: null
73
+ }
74
+ };
75
+ }
76
+ /**
77
+ * The inline config for a load: empty without `--config`; otherwise the
78
+ * project's Vite config (plugins, aliases, define) plus its env files.
79
+ */
80
+ async function inlineConfig({ config, mode }) {
81
+ const base = {
82
+ configFile: false,
83
+ logLevel: "silent"
84
+ };
85
+ if (config === void 0) return {
86
+ ...base,
87
+ ...mode === void 0 ? {} : { mode }
88
+ };
89
+ const chosen = mode ?? "development";
90
+ const user = (await loadConfigFromFile({
91
+ command: "serve",
92
+ mode: chosen
93
+ }, config))?.config ?? {};
94
+ const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
95
+ const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
96
+ return {
97
+ ...user,
98
+ ...base,
99
+ mode: chosen,
100
+ plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
101
+ };
102
+ }
103
+ /** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
104
+ async function inspectFile(file, options = {}, load = {}) {
105
+ if (load.config !== void 0 && !existsSync(load.config)) return {
106
+ machines: [],
107
+ warnings: [`${file}: Vite config not found: ${load.config}`],
108
+ failed: true
109
+ };
110
+ const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
111
+ ...inline,
112
+ plugins: [...inline.plugins ?? [], inspectEntry()]
113
+ })).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
114
+ if (Either.isLeft(loaded)) return {
115
+ machines: [],
116
+ warnings: [`${file}: ${loaded[0]}`],
117
+ failed: true
118
+ };
119
+ const { module } = loaded[1];
120
+ const run = runOf(module);
121
+ if (run === null) return {
122
+ machines: [],
123
+ warnings: [`${file}: could not load the inspector`],
124
+ failed: true
125
+ };
126
+ return {
127
+ ...await run(options),
128
+ failed: false
129
+ };
130
+ }
131
+ export { runOf as i, inspectEntry as n, inspectFile as r, entryId as t };
@@ -0,0 +1,14 @@
1
+ import { AnyActor } from "@mantaq/core";
2
+ type Made = AnyActor<unknown> | {
3
+ actor: AnyActor<unknown>;
4
+ };
5
+ /**
6
+ * Wrap a machine factory in a vitest test file so every passing test that
7
+ * builds an actor with it is recorded to `__runs__/<test file>.json` next to
8
+ * the test: the events it handled and how far its clock moved. Run the whole
9
+ * file to refresh it; with `CI` set a stale file fails the test instead.
10
+ * The factory may be called in the test or in `beforeEach`. A retried test is
11
+ * recorded once, from the attempt that passed.
12
+ */
13
+ export declare function recordRuns<F extends (...args: never[]) => Made>(factory: F): F;
14
+ export {};
@@ -0,0 +1,154 @@
1
+ import { r as machineId } from "./inspect-B7ZQCNM2.mjs";
2
+ import { Either } from "@mantaq/utils";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ // \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __exportAll = (all, no_symbols) => {
11
+ let target = {};
12
+ for (var name in all) __defProp(target, name, {
13
+ get: all[name],
14
+ enumerable: true
15
+ });
16
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
17
+ return target;
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
21
+ key = keys[i];
22
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
23
+ get: ((k) => from[k]).bind(null, key),
24
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
25
+ });
26
+ }
27
+ return to;
28
+ };
29
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
30
+ // ../../node_modules/.pnpm/vite-plus@1.4.0_@types+node@26.2.0_@typescript+native-preview@7.0.0-dev.20260707.2_@vit_594a0913d1d55d70a26e6ccc5f2ebd7d/node_modules/vite-plus/dist/test/index.js
31
+ var test_exports = /* @__PURE__ */ __exportAll({});
32
+ import * as import_vitest from "vitest";
33
+ __reExport(test_exports, import_vitest);
34
+ // src/record.ts
35
+ /** Actors built during the running test attempt, flushed when it ends. */
36
+ let pending = [];
37
+ /** Runs recorded so far, per test file, per machine id. */
38
+ const files = /* @__PURE__ */ new Map();
39
+ function actorOf(made) {
40
+ return "actor" in made ? made.actor : made;
41
+ }
42
+ function isJson(value) {
43
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
44
+ if (typeof value === "number") return Number.isFinite(value);
45
+ if (Array.isArray(value)) return value.every(isJson);
46
+ if (typeof value !== "object") return false;
47
+ const proto = Object.getPrototypeOf(value);
48
+ if (proto !== Object.prototype && proto !== null) return false;
49
+ return Object.values(value).every((each) => each === void 0 || isJson(each));
50
+ }
51
+ function jsonPayload(payload) {
52
+ if (payload !== void 0 && !isJson(payload)) return Either.left("payload is not JSON");
53
+ const text = Either.from(() => JSON.stringify(payload ?? null));
54
+ if (Either.isLeft(text)) return Either.left("payload is not JSON");
55
+ return Either.right(JSON.parse(text[1]));
56
+ }
57
+ function textOf(runs) {
58
+ const body = [...runs].sort(([a], [b]) => a.localeCompare(b)).map(([id, list]) => [id, [...list].sort((a, b) => a.name.localeCompare(b.name))]);
59
+ return `${JSON.stringify(Object.fromEntries(body), null, 2)}\n`;
60
+ }
61
+ function sidecarOf(testPath) {
62
+ return join(dirname(testPath), "__runs__", `${basename(testPath).replace(/\.[cm]?[jt]sx?$/, "")}.json`);
63
+ }
64
+ function check(file, id, run) {
65
+ const committed = Either.from(() => JSON.parse(readFileSync(file, "utf8")));
66
+ const list = Either.isRight(committed) ? committed[1]?.[id] : void 0;
67
+ const found = Array.isArray(list) ? list.find((each) => each?.name === run.name) : void 0;
68
+ if (JSON.stringify(found) !== JSON.stringify(run)) throw new Error(`recordRuns: ${file} is out of date for "${run.name}". Run the tests locally without CI set and commit the result.`);
69
+ }
70
+ function flush(testPath, id, run, runs) {
71
+ const file = sidecarOf(testPath);
72
+ if (process.env["CI"]) return check(file, id, run);
73
+ mkdirSync(dirname(file), { recursive: true });
74
+ writeFileSync(file, textOf(runs));
75
+ }
76
+ function finish(attempt, passed) {
77
+ if (!passed) return;
78
+ const counts = /* @__PURE__ */ new Map();
79
+ for (const each of attempt) {
80
+ if (each.problem !== "") {
81
+ console.warn(`recordRuns: "${each.name}" not recorded, ${each.problem}`);
82
+ continue;
83
+ }
84
+ const now = each.actor.clock.now();
85
+ if (now > each.seen) each.steps.push({
86
+ kind: "advance",
87
+ ms: now - each.seen
88
+ });
89
+ const count = (counts.get(each.name) ?? 0) + 1;
90
+ counts.set(each.name, count);
91
+ const name = count === 1 ? each.name : `${each.name} #${count}`;
92
+ const runs = files.get(each.testPath) ?? /* @__PURE__ */ new Map();
93
+ files.set(each.testPath, runs);
94
+ const list = runs.get(each.id) ?? [];
95
+ runs.set(each.id, list);
96
+ const run = {
97
+ name,
98
+ end: each.actor.state.name,
99
+ steps: each.steps
100
+ };
101
+ list.push(run);
102
+ flush(each.testPath, each.id, run, runs);
103
+ }
104
+ }
105
+ (0, test_exports.afterEach)(({ task }) => {
106
+ const attempt = pending;
107
+ pending = [];
108
+ finish(attempt, task.result?.state !== "fail");
109
+ });
110
+ /**
111
+ * Wrap a machine factory in a vitest test file so every passing test that
112
+ * builds an actor with it is recorded to `__runs__/<test file>.json` next to
113
+ * the test: the events it handled and how far its clock moved. Run the whole
114
+ * file to refresh it; with `CI` set a stale file fails the test instead.
115
+ * The factory may be called in the test or in `beforeEach`. A retried test is
116
+ * recorded once, from the attempt that passed.
117
+ */
118
+ function recordRuns(factory) {
119
+ const id = machineId(factory.name);
120
+ const wrapped = (...args) => {
121
+ const made = factory(...args);
122
+ const actor = actorOf(made);
123
+ const { testPath = "", currentTestName = "" } = test_exports.expect.getState();
124
+ const entry = {
125
+ actor,
126
+ id,
127
+ name: currentTestName,
128
+ testPath,
129
+ steps: [],
130
+ problem: "",
131
+ seen: actor.clock.now()
132
+ };
133
+ pending.push(entry);
134
+ actor.on("transition", { fn: ({ event }) => {
135
+ const now = actor.clock.now();
136
+ if (now > entry.seen) entry.steps.push({
137
+ kind: "advance",
138
+ ms: now - entry.seen
139
+ });
140
+ entry.seen = now;
141
+ const payload = jsonPayload(event.payload);
142
+ if (Either.isLeft(payload)) entry.problem = payload[0];
143
+ else entry.steps.push({
144
+ kind: "event",
145
+ event: event.type,
146
+ payload: payload[1]
147
+ });
148
+ } });
149
+ return made;
150
+ };
151
+ Object.defineProperty(wrapped, "name", { value: factory.name });
152
+ return wrapped;
153
+ }
154
+ export { recordRuns };
package/dist/vite.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as FuzzSettings } from "./inspect-DvQtAcwv.mjs";
1
+ import { t as FuzzSettings } from "./inspect-vSdt2qtL.mjs";
2
2
  import { Plugin } from "vite";
3
3
  export declare const STYLESHEETS: string[];
4
4
  export interface MantaqVizOptions {
package/dist/vite.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { c as projectRoot, i as runOf, l as vizDocument, n as inspectEntry, s as machineId, t as entryId, u as analyzeModule } from "./load-D0_Jl6Wr.mjs";
1
+ import { a as vizDocument, i as projectRoot, o as analyzeModule, r as machineId } from "./inspect-B7ZQCNM2.mjs";
2
+ import { i as runOf, n as inspectEntry, r as inspectFile, t as entryId } from "./load-CWubcSZU.mjs";
2
3
  import { UPDATE_EVENT, livePageHtml, serializeViz } from "@mantaq/viz";
3
4
  import { Either } from "@mantaq/utils";
4
- import { dirname, isAbsolute, resolve } from "node:path";
5
- import { globSync, readFileSync } from "node:fs";
5
+ import { dirname, isAbsolute, posix, resolve } from "node:path";
6
+ import { existsSync, globSync, readFileSync } from "node:fs";
6
7
  import { fileURLToPath } from "node:url";
7
8
  import { searchForWorkspaceRoot } from "vite";
8
9
  // src/vite.ts
@@ -13,6 +14,9 @@ import { searchForWorkspaceRoot } from "vite";
13
14
  * import { mantaqViz } from "@mantaq/inspect/vite";
14
15
  * export default defineConfig({ plugins: [mantaqViz({ machines: "src/**\/*.machine.ts" })] });
15
16
  *
17
+ * `vite build` with the plugin also writes the page as a static site
18
+ * (`index.html`, `viz.json`, a script) under `base`.
19
+ *
16
20
  * Open `/__mantaq/` while the dev server runs. Machines load through your own
17
21
  * Vite config (aliases, tsconfig paths), are inspected on the server, and the
18
22
  * page redraws, keeping the walk, when you save a file.
@@ -73,6 +77,64 @@ async function documentText(server, context) {
73
77
  warnings: results.flatMap((result) => result.warnings)
74
78
  };
75
79
  }
80
+ function stylesheets() {
81
+ return STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n");
82
+ }
83
+ /** Build mode: write the page, `viz.json` and the viewer script under `base`. */
84
+ function siteBuild(context) {
85
+ let standalone = false;
86
+ return {
87
+ name: "mantaq-viz-build",
88
+ apply: "build",
89
+ config: (config) => {
90
+ const input = config.build?.rollupOptions?.input;
91
+ const root = resolve(config.root ?? process.cwd());
92
+ standalone = input === void 0 && !existsSync(resolve(root, "index.html"));
93
+ return standalone ? { build: { rollupOptions: { input: { "mantaq-live": context.live } } } } : {};
94
+ },
95
+ buildStart() {
96
+ if (!standalone) this.emitFile({
97
+ type: "chunk",
98
+ id: context.live,
99
+ name: "mantaq-live"
100
+ });
101
+ },
102
+ async generateBundle(_options, bundle) {
103
+ const results = [];
104
+ for (const file of machineFiles(context.root(), context.patterns)) {
105
+ const load = { config: context.project.configFile };
106
+ try {
107
+ results.push(await inspectFile(file, { fuzz: context.fuzz }, load));
108
+ } catch (error) {
109
+ this.warn(`[mantaq-viz] ${file}: could not inspect (${String(error)})`);
110
+ }
111
+ }
112
+ for (const warning of results.flatMap((result) => result.warnings)) this.warn(`[mantaq-viz] ${warning}`);
113
+ const text = serializeViz(vizDocument(results.flatMap((result) => result.machines)));
114
+ const dir = context.base.replace(/^\//, "");
115
+ const entry = Object.values(bundle).find((chunk) => chunk.type === "chunk" && chunk.isEntry && chunk.name === "mantaq-live");
116
+ if (entry === void 0) {
117
+ this.error("[mantaq-viz] the live page entry was not bundled");
118
+ return;
119
+ }
120
+ this.emitFile({
121
+ type: "asset",
122
+ fileName: `${dir}viz.json`,
123
+ source: text
124
+ });
125
+ this.emitFile({
126
+ type: "asset",
127
+ fileName: `${dir}index.html`,
128
+ source: livePageHtml({
129
+ title: "mantaq viz",
130
+ source: "viz.json",
131
+ entry: posix.relative(dir === "" ? "." : dir, entry.fileName),
132
+ css: stylesheets()
133
+ })
134
+ });
135
+ }
136
+ };
137
+ }
76
138
  function mantaqViz(options) {
77
139
  const base = (options.base ?? "/__mantaq/").replace(/\/?$/, "/");
78
140
  const patterns = Array.isArray(options.machines) ? options.machines : [options.machines];
@@ -80,80 +142,92 @@ function mantaqViz(options) {
80
142
  const live = fileURLToPath(import.meta.resolve(import.meta.url.endsWith(".ts") ? "./live-entry.ts" : "./live-entry.mjs"));
81
143
  const viz = dirname(dirname(fileURLToPath(import.meta.resolve("@mantaq/viz/live"))));
82
144
  let root = process.cwd();
83
- return [
84
- inspectEntry(),
85
- {
86
- name: "mantaq-viz-targets",
87
- apply: "serve",
88
- configResolved: (config) => {
89
- root = config.root;
90
- },
91
- resolveId: (id) => id === "virtual:mantaq/targets" ? TARGETS_RESOLVED : null,
92
- load: (id) => id === "\0virtual:mantaq/targets" ? targetsSource(machineFiles(root, patterns)) : null
145
+ const project = { configFile: void 0 };
146
+ const targets = {
147
+ name: "mantaq-viz-targets",
148
+ configResolved: (config) => {
149
+ root = config.root;
150
+ project.configFile = config.configFile;
93
151
  },
94
- {
95
- name: "mantaq-viz",
96
- apply: "serve",
97
- config: (config) => ({ server: { fs: { allow: [
98
- searchForWorkspaceRoot(config.root ?? process.cwd()),
99
- dirname(dirname(live)),
100
- viz
101
- ] } } }),
102
- configureServer(server) {
103
- let cached = null;
104
- const build = async () => {
105
- const { text, warnings } = await documentText(server, {
106
- files: machineFiles(server.config.root, patterns),
107
- fuzz
108
- });
109
- for (const warning of warnings) server.config.logger.warn(`[mantaq-viz] ${warning}`);
110
- return text;
111
- };
112
- server.watcher.on("all", (_event, path) => {
113
- if (!/\.[cm]?[jt]sx?$/.test(path)) return;
114
- cached = null;
115
- const generated = server.moduleGraph.getModuleById(TARGETS_RESOLVED);
116
- if (generated !== void 0) server.moduleGraph.invalidateModule(generated);
117
- server.environments.ssr.moduleGraph.invalidateAll();
118
- server.ws.send({
119
- type: "custom",
120
- event: UPDATE_EVENT
121
- });
152
+ resolveId: (id) => id === "virtual:mantaq/targets" ? TARGETS_RESOLVED : null,
153
+ load: (id) => id === "\0virtual:mantaq/targets" ? targetsSource(machineFiles(root, patterns)) : null
154
+ };
155
+ const plugin = {
156
+ name: "mantaq-viz",
157
+ apply: "serve",
158
+ config: (config) => ({ server: { fs: { allow: [
159
+ searchForWorkspaceRoot(config.root ?? process.cwd()),
160
+ dirname(dirname(live)),
161
+ viz
162
+ ] } } }),
163
+ configureServer(server) {
164
+ let cached = null;
165
+ const build = async () => {
166
+ const { text, warnings } = await documentText(server, {
167
+ files: machineFiles(server.config.root, patterns),
168
+ fuzz
122
169
  });
123
- server.middlewares.use((request, response, next) => {
124
- const path = request.url?.split("?")[0];
125
- if (path === `${base}viz.json`) {
126
- cached ??= build();
127
- cached.then((text) => {
128
- response.setHeader("content-type", "application/json");
129
- response.end(text);
130
- }, (error) => {
131
- cached = null;
132
- response.statusCode = 500;
133
- response.end(String(error));
134
- });
135
- return;
136
- }
137
- if (path !== base && path !== base.slice(0, -1)) {
138
- next();
139
- return;
140
- }
141
- const html = livePageHtml({
142
- title: "mantaq viz",
143
- source: `${base}viz.json`,
144
- entry: `/@fs${live}`,
145
- css: STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n")
146
- });
147
- server.transformIndexHtml(request.url ?? base, html).then((page) => {
148
- response.setHeader("content-type", "text/html");
149
- response.end(page);
170
+ for (const warning of warnings) server.config.logger.warn(`[mantaq-viz] ${warning}`);
171
+ return text;
172
+ };
173
+ server.watcher.on("all", (_event, path) => {
174
+ if (!/\.[cm]?[jt]sx?$/.test(path)) return;
175
+ cached = null;
176
+ const generated = server.moduleGraph.getModuleById(TARGETS_RESOLVED);
177
+ if (generated !== void 0) server.moduleGraph.invalidateModule(generated);
178
+ server.environments.ssr.moduleGraph.invalidateAll();
179
+ server.ws.send({
180
+ type: "custom",
181
+ event: UPDATE_EVENT
182
+ });
183
+ });
184
+ server.middlewares.use((request, response, next) => {
185
+ const path = request.url?.split("?")[0];
186
+ if (path === `${base}viz.json`) {
187
+ cached ??= build();
188
+ cached.then((text) => {
189
+ response.setHeader("content-type", "application/json");
190
+ response.end(text);
191
+ }, (error) => {
192
+ cached = null;
193
+ response.statusCode = 500;
194
+ response.end(String(error));
150
195
  });
196
+ return;
197
+ }
198
+ if (path !== base && path !== base.slice(0, -1)) {
199
+ next();
200
+ return;
201
+ }
202
+ const html = livePageHtml({
203
+ title: "mantaq viz",
204
+ source: `${base}viz.json`,
205
+ entry: `/@fs${live}`,
206
+ css: stylesheets()
151
207
  });
152
- server.httpServer?.once("listening", () => {
153
- server.config.logger.info(` mantaq viz: ${base}`);
208
+ server.transformIndexHtml(request.url ?? base, html).then((page) => {
209
+ response.setHeader("content-type", "text/html");
210
+ response.end(page);
154
211
  });
155
- }
212
+ });
213
+ server.httpServer?.once("listening", () => {
214
+ server.config.logger.info(` mantaq viz: ${base}`);
215
+ });
156
216
  }
217
+ };
218
+ const site = siteBuild({
219
+ base,
220
+ patterns,
221
+ fuzz,
222
+ live,
223
+ project,
224
+ root: () => root
225
+ });
226
+ return [
227
+ inspectEntry(),
228
+ targets,
229
+ site,
230
+ plugin
157
231
  ];
158
232
  }
159
233
  export { STYLESHEETS, TARGETS, TARGETS_RESOLVED, mantaqViz, targetsSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantaq/inspect",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Turn a Mantaq machine into a viz document: structure, handler explanations (AST), fuzz and test coverage. Vite plugin included.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,7 @@
16
16
  ".": "./dist/index.mjs",
17
17
  "./drive": "./dist/drive.mjs",
18
18
  "./live-entry": "./dist/live-entry.mjs",
19
+ "./record": "./dist/record.mjs",
19
20
  "./vite": "./dist/vite.mjs",
20
21
  "./package.json": "./package.json"
21
22
  },
@@ -25,10 +26,10 @@
25
26
  "dependencies": {
26
27
  "typescript": "6.0.3",
27
28
  "@mantaq/core": "0.5.4",
29
+ "@mantaq/pbt": "0.0.4",
28
30
  "@mantaq/traversal": "0.3.4",
29
- "@mantaq/viz": "0.3.0",
30
31
  "@mantaq/utils": "0.0.5",
31
- "@mantaq/pbt": "0.0.4"
32
+ "@mantaq/viz": "0.4.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "^26.2.0",
@@ -36,10 +37,20 @@
36
37
  "happy-dom": "^21.0.4",
37
38
  "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
38
39
  "vite-plus": "1.4.0",
40
+ "vitest": "5.0.5",
39
41
  "@mantaq/sugar": "2.0.4"
40
42
  },
41
43
  "peerDependencies": {
42
- "vite": "*"
44
+ "vite": "*",
45
+ "vitest": "*"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "vitest": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "inlinedDependencies": {
53
+ "vite-plus": "1.4.0"
43
54
  },
44
55
  "scripts": {
45
56
  "build": "vp pack",