@mantaq/inspect 0.3.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 +25 -12
- package/dist/index.d.mts +38 -5
- package/dist/index.mjs +98 -3
- package/dist/{vite-BBW0CmYr.mjs → inspect-B7ZQCNM2.mjs} +155 -289
- package/dist/{inspect-DvQtAcwv.d.mts → inspect-vSdt2qtL.d.mts} +3 -3
- package/dist/live-entry.mjs +84 -2
- package/dist/load-CWubcSZU.mjs +131 -0
- package/dist/record.d.mts +14 -0
- package/dist/record.mjs +154 -0
- package/dist/vite.d.mts +1 -1
- package/dist/vite.mjs +233 -1
- package/package.json +19 -12
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -152
- package/dist/live-embedded.d.mts +0 -1
- package/dist/live-embedded.mjs +0 -16
- package/dist/live-page-DEDl_Nio.mjs +0 -85
- package/dist/page-CK7-IsLJ.mjs +0 -41
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
2
|
-
import { ANY_STATE,
|
|
1
|
+
import { i as execute, r as eventCatalog, t as applyStep } from "./run-B5huYj_r.mjs";
|
|
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 {
|
|
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,
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { loadConfigFromFile, loadEnv, runnerImport, searchForWorkspaceRoot } 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) {
|
|
@@ -407,6 +406,7 @@ function fuzz(target, options) {
|
|
|
407
406
|
}
|
|
408
407
|
}
|
|
409
408
|
consider([], 0);
|
|
409
|
+
for (const steps of options.initial ?? []) consider([...steps], 0);
|
|
410
410
|
for (let iteration = 1; iteration <= options.iterations; iteration += 1) {
|
|
411
411
|
const at = context.rng.int(corpus.length);
|
|
412
412
|
consider(mutate({
|
|
@@ -1556,6 +1556,107 @@ function annotateWith(machine, context) {
|
|
|
1556
1556
|
edges
|
|
1557
1557
|
};
|
|
1558
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
|
+
}
|
|
1559
1660
|
// src/inspect.ts
|
|
1560
1661
|
const DEFAULT_ITERATIONS = 2e3;
|
|
1561
1662
|
function isVirtualClock(clock) {
|
|
@@ -1600,10 +1701,11 @@ function driven(target, context) {
|
|
|
1600
1701
|
const fuzzed = settings === false ? structure : fuzzedMachine(run[1], fuzz(run[1], {
|
|
1601
1702
|
seed: settings?.seed ?? DEFAULT_SEED,
|
|
1602
1703
|
iterations: settings?.iterations ?? DEFAULT_ITERATIONS,
|
|
1704
|
+
...target.tests === void 0 ? {} : { initial: target.tests.map((run) => run.steps) },
|
|
1603
1705
|
...guidance(structure)
|
|
1604
1706
|
}));
|
|
1605
1707
|
return {
|
|
1606
|
-
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)),
|
|
1607
1709
|
skipped: []
|
|
1608
1710
|
};
|
|
1609
1711
|
}
|
|
@@ -1622,7 +1724,10 @@ function inspectWith(target, analysis) {
|
|
|
1622
1724
|
});
|
|
1623
1725
|
const quiet = target.fuzz === false && target.tests === void 0;
|
|
1624
1726
|
const unanalyzed = analysis === null ? [] : [Either.getLeft(analysis)].filter((each) => each !== void 0);
|
|
1625
|
-
const annotated = annotate(
|
|
1727
|
+
const annotated = annotate({
|
|
1728
|
+
...machine,
|
|
1729
|
+
runs: [...target.tests ?? []]
|
|
1730
|
+
});
|
|
1626
1731
|
return {
|
|
1627
1732
|
machine: annotated,
|
|
1628
1733
|
warnings: [
|
|
@@ -1676,6 +1781,33 @@ function actorFactory(found, value) {
|
|
|
1676
1781
|
return isActor(actor) ? actor : sample;
|
|
1677
1782
|
};
|
|
1678
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
|
+
}
|
|
1679
1811
|
/**
|
|
1680
1812
|
* Every machine a file exports, found from its types: actors, and functions
|
|
1681
1813
|
* that build one (optionally taking a clock or `{ clock }`). When a file has
|
|
@@ -1701,6 +1833,7 @@ async function inspectModule(file, options) {
|
|
|
1701
1833
|
warnings: [`${file}: no exported actor or actor factory found`]
|
|
1702
1834
|
};
|
|
1703
1835
|
const exports = await options.load(file);
|
|
1836
|
+
const { runs: recorded, warnings: runWarnings } = readRuns(dirname(file));
|
|
1704
1837
|
const results = chosen.map((each) => {
|
|
1705
1838
|
const id = machineId(each.name);
|
|
1706
1839
|
const create = actorFactory(each, exports[each.name]);
|
|
@@ -1709,21 +1842,30 @@ async function inspectModule(file, options) {
|
|
|
1709
1842
|
warnings: [`${id}: ${each.name} did not return an actor`]
|
|
1710
1843
|
};
|
|
1711
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);
|
|
1712
1849
|
const inspected = inspectWith({
|
|
1713
1850
|
id,
|
|
1714
1851
|
create,
|
|
1715
1852
|
source,
|
|
1716
|
-
fuzz: fuzzSettings
|
|
1853
|
+
fuzz: fuzzSettings,
|
|
1854
|
+
...replayed.runs.length === 0 ? {} : { tests: replayed.runs }
|
|
1717
1855
|
}, analysis);
|
|
1718
1856
|
const instance = each.call === "instance" ? ["not fuzzed: exported as one actor, not a factory"] : [];
|
|
1719
1857
|
return {
|
|
1720
1858
|
machine: inspected.machine,
|
|
1721
|
-
warnings: [
|
|
1859
|
+
warnings: [
|
|
1860
|
+
...instance,
|
|
1861
|
+
...replayed.warnings,
|
|
1862
|
+
...inspected.warnings
|
|
1863
|
+
].map((warning) => `${id}: ${warning}`)
|
|
1722
1864
|
};
|
|
1723
1865
|
});
|
|
1724
1866
|
return {
|
|
1725
1867
|
machines: results.flatMap((result) => result.machine === null ? [] : [result.machine]),
|
|
1726
|
-
warnings: results.flatMap((result) => result.warnings)
|
|
1868
|
+
warnings: [...runWarnings.map((warning) => `${file}: ${warning}`), ...results.flatMap((result) => result.warnings)]
|
|
1727
1869
|
};
|
|
1728
1870
|
}
|
|
1729
1871
|
/** Machines as a document, ready for `serializeViz`. */
|
|
@@ -1733,280 +1875,4 @@ function vizDocument(machines) {
|
|
|
1733
1875
|
machines: [...machines]
|
|
1734
1876
|
};
|
|
1735
1877
|
}
|
|
1736
|
-
|
|
1737
|
-
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
1738
|
-
const ENTRY = "virtual:mantaq-inspect";
|
|
1739
|
-
const RESOLVED = `\0${ENTRY}`;
|
|
1740
|
-
/**
|
|
1741
|
-
* A module that imports the machine file and the inspector side by side. Run
|
|
1742
|
-
* through one module graph, both see the same `@mantaq/core` instance: core
|
|
1743
|
-
* recognizes its state refs by a module-level registry, so a machine loaded
|
|
1744
|
-
* through a bundler and inspected from plain Node would look stateless.
|
|
1745
|
-
*/
|
|
1746
|
-
function entrySource(file) {
|
|
1747
|
-
const inspect = fileURLToPath(import.meta.resolve("@mantaq/inspect"));
|
|
1748
|
-
return [
|
|
1749
|
-
`import * as machines from ${JSON.stringify(file)};`,
|
|
1750
|
-
`import { inspectModule } from ${JSON.stringify(inspect)};`,
|
|
1751
|
-
`export function run(options) {`,
|
|
1752
|
-
` return inspectModule(${JSON.stringify(file)}, { ...options, load: async () => machines });`,
|
|
1753
|
-
`}`
|
|
1754
|
-
].join("\n");
|
|
1755
|
-
}
|
|
1756
|
-
/** Vite plugin serving the entry for each `virtual:mantaq-inspect?file=<path>` id. */
|
|
1757
|
-
function inspectEntry() {
|
|
1758
|
-
return {
|
|
1759
|
-
name: "mantaq-inspect-entry",
|
|
1760
|
-
resolveId: (id) => id.startsWith(ENTRY) ? `\0${id}` : null,
|
|
1761
|
-
load: (id) => {
|
|
1762
|
-
if (!id.startsWith(RESOLVED)) return null;
|
|
1763
|
-
const file = new URLSearchParams(id.slice(id.indexOf("?") + 1)).get("file");
|
|
1764
|
-
return file === null ? null : entrySource(file);
|
|
1765
|
-
}
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
function entryId(file) {
|
|
1769
|
-
return `${ENTRY}?${new URLSearchParams({ file }).toString()}`;
|
|
1770
|
-
}
|
|
1771
|
-
/** The `run` export of an entry module, checked: module objects are untyped. */
|
|
1772
|
-
function runOf(module) {
|
|
1773
|
-
const run = typeof module === "object" && module !== null ? Reflect.get(module, "run") : void 0;
|
|
1774
|
-
if (typeof run !== "function") return null;
|
|
1775
|
-
return async (options) => Reflect.apply(run, void 0, [options]);
|
|
1776
|
-
}
|
|
1777
|
-
const ENV_ACCESS = [
|
|
1778
|
-
"import",
|
|
1779
|
-
"meta",
|
|
1780
|
-
"env"
|
|
1781
|
-
].join(".");
|
|
1782
|
-
/**
|
|
1783
|
-
* Replaces the env object with the project's env: `runnerImport` never reads
|
|
1784
|
-
* env files, so `VITE_*` variables would otherwise be missing. A textual
|
|
1785
|
-
* replace: it also rewrites matches in strings and comments, and a frozen
|
|
1786
|
-
* snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
|
|
1787
|
-
*/
|
|
1788
|
-
function projectEnv(env, mode) {
|
|
1789
|
-
const value = JSON.stringify({
|
|
1790
|
-
...env,
|
|
1791
|
-
BASE_URL: "/",
|
|
1792
|
-
MODE: mode,
|
|
1793
|
-
DEV: true,
|
|
1794
|
-
PROD: false,
|
|
1795
|
-
SSR: true
|
|
1796
|
-
});
|
|
1797
|
-
return {
|
|
1798
|
-
name: "mantaq-inspect-env",
|
|
1799
|
-
enforce: "pre",
|
|
1800
|
-
transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
|
|
1801
|
-
code: code.replaceAll(ENV_ACCESS, `(${value})`),
|
|
1802
|
-
map: null
|
|
1803
|
-
}
|
|
1804
|
-
};
|
|
1805
|
-
}
|
|
1806
|
-
/**
|
|
1807
|
-
* The inline config for a load: empty without `--config`; otherwise the
|
|
1808
|
-
* project's Vite config (plugins, aliases, define) plus its env files.
|
|
1809
|
-
*/
|
|
1810
|
-
async function inlineConfig({ config, mode }) {
|
|
1811
|
-
const base = {
|
|
1812
|
-
configFile: false,
|
|
1813
|
-
logLevel: "silent"
|
|
1814
|
-
};
|
|
1815
|
-
if (config === void 0) return {
|
|
1816
|
-
...base,
|
|
1817
|
-
...mode === void 0 ? {} : { mode }
|
|
1818
|
-
};
|
|
1819
|
-
const chosen = mode ?? "development";
|
|
1820
|
-
const user = (await loadConfigFromFile({
|
|
1821
|
-
command: "serve",
|
|
1822
|
-
mode: chosen
|
|
1823
|
-
}, config))?.config ?? {};
|
|
1824
|
-
const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
|
|
1825
|
-
const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
|
|
1826
|
-
return {
|
|
1827
|
-
...user,
|
|
1828
|
-
...base,
|
|
1829
|
-
mode: chosen,
|
|
1830
|
-
plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
|
|
1831
|
-
};
|
|
1832
|
-
}
|
|
1833
|
-
/** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
|
|
1834
|
-
async function inspectFile(file, options = {}, load = {}) {
|
|
1835
|
-
if (load.config !== void 0 && !existsSync(load.config)) return {
|
|
1836
|
-
machines: [],
|
|
1837
|
-
warnings: [`${file}: Vite config not found: ${load.config}`],
|
|
1838
|
-
failed: true
|
|
1839
|
-
};
|
|
1840
|
-
const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
|
|
1841
|
-
...inline,
|
|
1842
|
-
plugins: [...inline.plugins ?? [], inspectEntry()]
|
|
1843
|
-
})).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
|
|
1844
|
-
if (Either.isLeft(loaded)) return {
|
|
1845
|
-
machines: [],
|
|
1846
|
-
warnings: [`${file}: ${loaded[0]}`],
|
|
1847
|
-
failed: true
|
|
1848
|
-
};
|
|
1849
|
-
const { module } = loaded[1];
|
|
1850
|
-
const run = runOf(module);
|
|
1851
|
-
if (run === null) return {
|
|
1852
|
-
machines: [],
|
|
1853
|
-
warnings: [`${file}: could not load the inspector`],
|
|
1854
|
-
failed: true
|
|
1855
|
-
};
|
|
1856
|
-
return {
|
|
1857
|
-
...await run(options),
|
|
1858
|
-
failed: false
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
// src/vite.ts
|
|
1862
|
-
/**
|
|
1863
|
-
* Vite plugin: a live page of your machines on your own dev server.
|
|
1864
|
-
*
|
|
1865
|
-
* // vite.config.ts
|
|
1866
|
-
* import { mantaqViz } from "@mantaq/inspect/vite";
|
|
1867
|
-
* export default defineConfig({ plugins: [mantaqViz({ machines: "src/**\/*.machine.ts" })] });
|
|
1868
|
-
*
|
|
1869
|
-
* Open `/__mantaq/` while the dev server runs. Machines load through your own
|
|
1870
|
-
* Vite config (aliases, tsconfig paths), are inspected on the server, and the
|
|
1871
|
-
* page redraws, keeping the walk, when you save a file.
|
|
1872
|
-
*/
|
|
1873
|
-
const STYLESHEETS = ["@mantaq/viz/viewer.css", "@mantaq/viz/page.css"];
|
|
1874
|
-
const DEV_ITERATIONS = 500;
|
|
1875
|
-
const TARGETS = "virtual:mantaq/targets";
|
|
1876
|
-
const TARGETS_RESOLVED = `\0${TARGETS}`;
|
|
1877
|
-
function createExpression(found, index) {
|
|
1878
|
-
const callee = `m${index}[${JSON.stringify(found.name)}]`;
|
|
1879
|
-
const call = found.call === "clock" ? `${callee}(VirtualClock())` : found.call === "options" ? `${callee}({ clock: VirtualClock() })` : `${callee}()`;
|
|
1880
|
-
return found.unwrap === "actor" ? `${call}.actor` : call;
|
|
1881
|
-
}
|
|
1882
|
-
/**
|
|
1883
|
-
* Source of `virtual:mantaq/targets`: one factory per exported actor factory,
|
|
1884
|
-
* shaped by its type (`call`, `clock`, `options`). A bare actor export cannot
|
|
1885
|
-
* be recreated, so it gets none. Like the server-side inspection, plain actors
|
|
1886
|
-
* are skipped when a file has factories.
|
|
1887
|
-
*/
|
|
1888
|
-
function targetsSource(files) {
|
|
1889
|
-
const core = fileURLToPath(import.meta.resolve("@mantaq/core"));
|
|
1890
|
-
const imports = [];
|
|
1891
|
-
const entries = [];
|
|
1892
|
-
files.forEach((file, index) => {
|
|
1893
|
-
const analysis = analyzeModule({
|
|
1894
|
-
file,
|
|
1895
|
-
root: projectRoot(file)
|
|
1896
|
-
});
|
|
1897
|
-
if (Either.isLeft(analysis)) return;
|
|
1898
|
-
const factories = analysis[1].machines.filter((found) => found.call !== "instance");
|
|
1899
|
-
if (factories.length === 0) return;
|
|
1900
|
-
imports.push(`import * as m${index} from ${JSON.stringify(file)};`);
|
|
1901
|
-
for (const found of factories) entries.push(` ${JSON.stringify(machineId(found.name))}: () => { const actor = ${createExpression(found, index)}; return { actor, clock: actor.clock }; },`);
|
|
1902
|
-
});
|
|
1903
|
-
return [
|
|
1904
|
-
`import { VirtualClock } from ${JSON.stringify(core)};`,
|
|
1905
|
-
...imports,
|
|
1906
|
-
"export const targets = {",
|
|
1907
|
-
...entries,
|
|
1908
|
-
"};"
|
|
1909
|
-
].join("\n");
|
|
1910
|
-
}
|
|
1911
|
-
function machineFiles(root, patterns) {
|
|
1912
|
-
const files = patterns.flatMap((pattern) => globSync(pattern, { cwd: root }));
|
|
1913
|
-
return [...new Set(files.map((file) => isAbsolute(file) ? file : resolve(root, file)))].sort();
|
|
1914
|
-
}
|
|
1915
|
-
async function documentText(server, context) {
|
|
1916
|
-
const results = [];
|
|
1917
|
-
for (const file of context.files) {
|
|
1918
|
-
const run = runOf(await server.ssrLoadModule(entryId(file)));
|
|
1919
|
-
results.push(run === null ? {
|
|
1920
|
-
machines: [],
|
|
1921
|
-
warnings: [`${file}: could not load the inspector`]
|
|
1922
|
-
} : await run({ fuzz: context.fuzz }));
|
|
1923
|
-
}
|
|
1924
|
-
return {
|
|
1925
|
-
text: serializeViz(vizDocument(results.flatMap((result) => result.machines))),
|
|
1926
|
-
warnings: results.flatMap((result) => result.warnings)
|
|
1927
|
-
};
|
|
1928
|
-
}
|
|
1929
|
-
function mantaqViz(options) {
|
|
1930
|
-
const base = (options.base ?? "/__mantaq/").replace(/\/?$/, "/");
|
|
1931
|
-
const patterns = Array.isArray(options.machines) ? options.machines : [options.machines];
|
|
1932
|
-
const fuzz = options.fuzz ?? { iterations: DEV_ITERATIONS };
|
|
1933
|
-
const live = fileURLToPath(import.meta.resolve(import.meta.url.endsWith(".ts") ? "./live-entry.ts" : "./live-entry.mjs"));
|
|
1934
|
-
const viz = dirname(dirname(fileURLToPath(import.meta.resolve("@mantaq/viz/live"))));
|
|
1935
|
-
let root = process.cwd();
|
|
1936
|
-
return [
|
|
1937
|
-
inspectEntry(),
|
|
1938
|
-
{
|
|
1939
|
-
name: "mantaq-viz-targets",
|
|
1940
|
-
apply: "serve",
|
|
1941
|
-
configResolved: (config) => {
|
|
1942
|
-
root = config.root;
|
|
1943
|
-
},
|
|
1944
|
-
resolveId: (id) => id === "virtual:mantaq/targets" ? TARGETS_RESOLVED : null,
|
|
1945
|
-
load: (id) => id === "\0virtual:mantaq/targets" ? targetsSource(machineFiles(root, patterns)) : null
|
|
1946
|
-
},
|
|
1947
|
-
{
|
|
1948
|
-
name: "mantaq-viz",
|
|
1949
|
-
apply: "serve",
|
|
1950
|
-
config: (config) => ({ server: { fs: { allow: [
|
|
1951
|
-
searchForWorkspaceRoot(config.root ?? process.cwd()),
|
|
1952
|
-
dirname(dirname(live)),
|
|
1953
|
-
viz
|
|
1954
|
-
] } } }),
|
|
1955
|
-
configureServer(server) {
|
|
1956
|
-
let cached = null;
|
|
1957
|
-
const build = async () => {
|
|
1958
|
-
const { text, warnings } = await documentText(server, {
|
|
1959
|
-
files: machineFiles(server.config.root, patterns),
|
|
1960
|
-
fuzz
|
|
1961
|
-
});
|
|
1962
|
-
for (const warning of warnings) server.config.logger.warn(`[mantaq-viz] ${warning}`);
|
|
1963
|
-
return text;
|
|
1964
|
-
};
|
|
1965
|
-
server.watcher.on("all", (_event, path) => {
|
|
1966
|
-
if (!/\.[cm]?[jt]sx?$/.test(path)) return;
|
|
1967
|
-
cached = null;
|
|
1968
|
-
const generated = server.moduleGraph.getModuleById(TARGETS_RESOLVED);
|
|
1969
|
-
if (generated !== void 0) server.moduleGraph.invalidateModule(generated);
|
|
1970
|
-
server.environments.ssr.moduleGraph.invalidateAll();
|
|
1971
|
-
server.ws.send({
|
|
1972
|
-
type: "custom",
|
|
1973
|
-
event: UPDATE_EVENT
|
|
1974
|
-
});
|
|
1975
|
-
});
|
|
1976
|
-
server.middlewares.use((request, response, next) => {
|
|
1977
|
-
const path = request.url?.split("?")[0];
|
|
1978
|
-
if (path === `${base}viz.json`) {
|
|
1979
|
-
cached ??= build();
|
|
1980
|
-
cached.then((text) => {
|
|
1981
|
-
response.setHeader("content-type", "application/json");
|
|
1982
|
-
response.end(text);
|
|
1983
|
-
}, (error) => {
|
|
1984
|
-
cached = null;
|
|
1985
|
-
response.statusCode = 500;
|
|
1986
|
-
response.end(String(error));
|
|
1987
|
-
});
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
if (path !== base && path !== base.slice(0, -1)) {
|
|
1991
|
-
next();
|
|
1992
|
-
return;
|
|
1993
|
-
}
|
|
1994
|
-
const html = livePageHtml({
|
|
1995
|
-
title: "mantaq viz",
|
|
1996
|
-
source: `${base}viz.json`,
|
|
1997
|
-
entry: `/@fs${live}`,
|
|
1998
|
-
css: STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n")
|
|
1999
|
-
});
|
|
2000
|
-
server.transformIndexHtml(request.url ?? base, html).then((page) => {
|
|
2001
|
-
response.setHeader("content-type", "text/html");
|
|
2002
|
-
response.end(page);
|
|
2003
|
-
});
|
|
2004
|
-
});
|
|
2005
|
-
server.httpServer?.once("listening", () => {
|
|
2006
|
-
server.config.logger.info(` mantaq viz: ${base}`);
|
|
2007
|
-
});
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
];
|
|
2011
|
-
}
|
|
2012
|
-
export { toViz as C, shrink as S, guidance as _, targetsSource as a, withTestCoverage as b, inspect as c, vizDocument as d, analyzeModule as f, branchDistanceGuide as g, branchDistance as h, mantaqViz as i, inspectModule as l, annotateMachine as m, TARGETS as n, inlineConfig as o, analyzeSource as p, TARGETS_RESOLVED as r, inspectFile as s, STYLESHEETS as t, machineId as u, literalPool as v, toVizMachine as w, fuzz as x, fuzzedMachine 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,
|
|
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
|
-
/**
|
|
27
|
-
tests?: ReadonlyArray<
|
|
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
|
}
|
package/dist/live-entry.mjs
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { UPDATE_EVENT } from "@mantaq/viz";
|
|
1
|
+
import { createDriver } from "./drive.mjs";
|
|
2
|
+
import { UPDATE_EVENT, parseViz } from "@mantaq/viz";
|
|
3
|
+
import { Either } from "@mantaq/utils";
|
|
3
4
|
import { targets } from "virtual:mantaq/targets";
|
|
5
|
+
import { mountViewer, showIssues } from "@mantaq/viz/viewer";
|
|
6
|
+
import { linkOptions } from "@mantaq/viz/page";
|
|
7
|
+
// src/live-page.ts
|
|
8
|
+
/**
|
|
9
|
+
* The live page of the Vite plugin: the served document, with each machine
|
|
10
|
+
* that has a target running for real behind the viewer. The walk lives in the
|
|
11
|
+
* address bar (`?machine=&step=`), so a redraw after a save replays it on the
|
|
12
|
+
* new code.
|
|
13
|
+
*/
|
|
14
|
+
/** A document holding only `machine`, so the viewer shows the machine its driver runs. */
|
|
15
|
+
function only(viz, machine) {
|
|
16
|
+
const chosen = viz.machines.find((each) => each.id === machine) ?? viz.machines[0];
|
|
17
|
+
return {
|
|
18
|
+
...viz,
|
|
19
|
+
machines: chosen === void 0 ? [] : [chosen]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Mount the document at `text`; machines with a target run live, the rest stay static walks. */
|
|
23
|
+
function mountDriven(root, context) {
|
|
24
|
+
const parsed = parseViz(context.text);
|
|
25
|
+
if (Either.isLeft(parsed)) {
|
|
26
|
+
showIssues(root, {
|
|
27
|
+
source: context.source,
|
|
28
|
+
issues: parsed[0]
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const options = linkOptions();
|
|
33
|
+
const viz = parsed[1];
|
|
34
|
+
const shown = only(viz, options.machine ?? null);
|
|
35
|
+
const id = shown.machines[0]?.id ?? "";
|
|
36
|
+
const target = context.targets[id];
|
|
37
|
+
const create = target !== void 0 && typeof Reflect.get(target().clock, "advance") === "function" ? target : void 0;
|
|
38
|
+
const picker = viz.machines.length > 1;
|
|
39
|
+
const select = (machine) => {
|
|
40
|
+
const next = new URLSearchParams(location.search);
|
|
41
|
+
next.set("machine", machine);
|
|
42
|
+
next.delete("step");
|
|
43
|
+
history.replaceState(null, "", `?${next.toString()}`);
|
|
44
|
+
mountDriven(root, context);
|
|
45
|
+
};
|
|
46
|
+
const doc = root.ownerDocument;
|
|
47
|
+
const bar = doc.createElement("nav");
|
|
48
|
+
bar.className = "machines";
|
|
49
|
+
if (picker) for (const machine of viz.machines) {
|
|
50
|
+
const button = doc.createElement("button");
|
|
51
|
+
button.type = "button";
|
|
52
|
+
button.textContent = machine.id;
|
|
53
|
+
button.disabled = machine.id === id;
|
|
54
|
+
button.addEventListener("click", () => select(machine.id));
|
|
55
|
+
bar.append(button);
|
|
56
|
+
}
|
|
57
|
+
const viewer = doc.createElement("div");
|
|
58
|
+
root.replaceChildren(...picker ? [bar] : [], viewer);
|
|
59
|
+
mountViewer(viewer, shown, {
|
|
60
|
+
...options,
|
|
61
|
+
...create === void 0 ? {} : { driver: createDriver({
|
|
62
|
+
id,
|
|
63
|
+
create
|
|
64
|
+
}) }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Fetch the document from `source` and mount it; call again to redraw, keeping the walk. */
|
|
68
|
+
async function mountLiveDriven(root, context) {
|
|
69
|
+
const response = await fetch(context.source).catch(() => null);
|
|
70
|
+
if (response === null || !response.ok) {
|
|
71
|
+
const status = response === null ? "network error" : `HTTP ${response.status}`;
|
|
72
|
+
showIssues(root, {
|
|
73
|
+
source: context.source,
|
|
74
|
+
issues: [{
|
|
75
|
+
path: "$",
|
|
76
|
+
message: status
|
|
77
|
+
}]
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
mountDriven(root, {
|
|
82
|
+
...context,
|
|
83
|
+
text: await response.text()
|
|
84
|
+
});
|
|
85
|
+
}
|
|
4
86
|
// src/live-entry.ts
|
|
5
87
|
/**
|
|
6
88
|
* Entry of the page `@mantaq/inspect/vite` serves. Redraws when the dev
|