@mantaq/inspect 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -10
- package/dist/index.d.mts +37 -4
- package/dist/index.mjs +97 -3
- package/dist/live-entry.mjs +84 -2
- package/dist/{vite-BBW0CmYr.mjs → load-D0_Jl6Wr.mjs} +7 -156
- package/dist/vite.mjs +159 -1
- package/package.json +8 -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
package/README.md
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
# @mantaq/inspect
|
|
2
2
|
|
|
3
|
-
Turns your Mantaq machine into a picture you can walk:
|
|
3
|
+
Turns your Mantaq machine into a picture you can walk: add the Vite plugin, no other config.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
mantaq-inspect src/cart.ts # writes mantaq-viz.html and opens it
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
It finds every exported actor, or function that builds one, from the file's types. It reads handler explanations and sample payloads from the source, fuzzes each machine that runs on a `VirtualClock`, and writes one self-contained HTML page that works offline: commit it, attach it to a PR. The viewer is [`@mantaq/viz`](../viz/README.md).
|
|
5
|
+
It finds every exported actor, or function that builds one, from the file's types. It reads handler explanations and sample payloads from the source, fuzzes each machine that runs on a `VirtualClock`, and shows them in a live page on your Vite dev server. The viewer is [`@mantaq/viz`](../viz/README.md).
|
|
10
6
|
|
|
11
|
-
##
|
|
7
|
+
## Ways in
|
|
12
8
|
|
|
13
|
-
**
|
|
9
|
+
**Gate in tests**: see the docs page "Design gate in vitest" for `expectNoDesignErrors`, `expectReached` and `expectAllStatesReached`.
|
|
14
10
|
|
|
15
11
|
**Vite plugin**: a live page on your dev server that redraws, keeping your walk, when you save.
|
|
16
12
|
|
|
@@ -43,14 +39,14 @@ const { machine, warnings } = inspect({
|
|
|
43
39
|
const json = serializeViz(vizDocument([machine]));
|
|
44
40
|
```
|
|
45
41
|
|
|
46
|
-
`inspectFile(file)` / `inspectModule(file, { load })` do the discovery the
|
|
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.
|
|
47
43
|
|
|
48
44
|
## What it needs from your file
|
|
49
45
|
|
|
50
46
|
- **Exports.** An actor, or a function that returns one (or `{ actor }`). Functions are called with no arguments, with a `VirtualClock` when the first parameter is a clock, or with `{ clock }` when it is an options object with a `clock`. Functions with other required parameters are skipped. Discovery reads types only; nothing is called to find out.
|
|
51
47
|
- **A fresh actor per call**, to fuzz. A file that only exports one actor instance is drawn, not fuzzed. When a file has factories, its actor instances are skipped.
|
|
52
48
|
- **Exported factories.** Export a factory that takes ports and a clock, not only a built instance. See [Machines in an application](https://anderscan.github.io/mantaq/guides/machines-in-an-app/).
|
|
53
|
-
- **A factory, for
|
|
49
|
+
- **A factory, for the live page.** The page recreates the actor on every step, so it needs an exported factory; a file that only exports an instance stays a static walk, with the usual warning.
|
|
54
50
|
- **A `VirtualClock`**, to fuzz. An actor on the real clock is drawn, with a warning.
|
|
55
51
|
|
|
56
52
|
Every part that cannot run is skipped with a warning, never guessed.
|
|
@@ -59,6 +55,7 @@ Every part that cannot run is skipped with a warning, never guessed.
|
|
|
59
55
|
|
|
60
56
|
The viewer's "Coverage from" picker dims elements with zero hits. What dimmed means depends on the source:
|
|
61
57
|
|
|
58
|
+
- **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`.
|
|
62
59
|
- **example tests** (`tested`): no test exercises this. Collected by replaying the event sequences you pass as `tests`, each on a fresh actor.
|
|
63
60
|
- **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.
|
|
64
61
|
|
package/dist/index.d.mts
CHANGED
|
@@ -37,6 +37,8 @@ interface FuzzOptions {
|
|
|
37
37
|
* the run in the corpus as a stepping stone even without new coverage.
|
|
38
38
|
*/
|
|
39
39
|
guide?: (trace: Trace) => boolean;
|
|
40
|
+
/** Known-good sequences (e.g. example tests) the corpus starts from, before any mutation. */
|
|
41
|
+
initial?: ReadonlyArray<ReadonlyArray<VizStep>>;
|
|
40
42
|
}
|
|
41
43
|
interface CorpusEntry {
|
|
42
44
|
steps: VizStep[];
|
|
@@ -149,8 +151,39 @@ interface LoadOptions {
|
|
|
149
151
|
/** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
|
|
150
152
|
export declare function inspectFile(file: string, options?: FileOptions, load?: LoadOptions): Promise<LoadedFile>;
|
|
151
153
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
+
* States no edge path leads to from the initial state. Empty when an undetermined
|
|
155
|
+
* edge hides the answer. Region states are never reported: the graph does not
|
|
156
|
+
* record where a region starts.
|
|
154
157
|
*/
|
|
155
|
-
export declare function
|
|
156
|
-
|
|
158
|
+
export declare function unreachableStates(machine: VizMachine): string[];
|
|
159
|
+
/** Design errors of an inspected machine, one line each. Unreached edges and routing warnings are not errors. */
|
|
160
|
+
export declare function designErrors(machine: VizMachine): string[];
|
|
161
|
+
/**
|
|
162
|
+
* Call from a test: throws when fuzzing finds a handler or effect that throws,
|
|
163
|
+
* an effect sends an event its state does not handle, or a declared state has
|
|
164
|
+
* no path from the initial state. Runs are seeded, so the result is stable.
|
|
165
|
+
*/
|
|
166
|
+
export declare function expectNoDesignErrors(target: InspectTarget): void;
|
|
167
|
+
interface ReachTransition {
|
|
168
|
+
from: string;
|
|
169
|
+
event: string;
|
|
170
|
+
/** Omit to accept any target. */
|
|
171
|
+
to?: string;
|
|
172
|
+
}
|
|
173
|
+
interface ReachExpectation {
|
|
174
|
+
states?: ReadonlyArray<string>;
|
|
175
|
+
transitions?: ReadonlyArray<ReachTransition>;
|
|
176
|
+
}
|
|
177
|
+
/** Ids of the states a fuzz run or the supplied test sequences visited. */
|
|
178
|
+
export declare function reachedStates(machine: VizMachine): string[];
|
|
179
|
+
/** What the expectation asks for and the machine did not reach, one line each. */
|
|
180
|
+
export declare function missedReach(machine: VizMachine, expected: ReachExpectation): string[];
|
|
181
|
+
/**
|
|
182
|
+
* Call from a test: throws unless fuzzing or the target's `tests` visited every
|
|
183
|
+
* listed state and took every listed transition. Reaching is proof; a miss only
|
|
184
|
+
* says nothing got there yet.
|
|
185
|
+
*/
|
|
186
|
+
export declare function expectReached(target: InspectTarget, expected: ReachExpectation): void;
|
|
187
|
+
/** Call from a test: throws unless fuzzing or the target's `tests` visited every declared state. */
|
|
188
|
+
export declare function expectAllStatesReached(target: InspectTarget): void;
|
|
189
|
+
export { type AnalysisSource, type CorpusEntry, type FileOptions, type FuzzOptions, type FuzzResult, type FuzzSettings, type FuzzTarget, type HandlerSite, type InspectTarget, type Inspected, type InspectedModule, type LiteralPool, type MachineExport, type ModuleAnalysis, type ModuleOptions, type Observation, type ReachExpectation, type ReachTransition, type RunError, type Trace, type VizInput, eventCatalog, execute, inspect, inspectModule, machineId, vizDocument };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,98 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
2
|
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { coveredByAny, unhandledSends } from "@mantaq/viz";
|
|
4
|
+
// src/gate.ts
|
|
5
|
+
/**
|
|
6
|
+
* States no edge path leads to from the initial state. Empty when an undetermined
|
|
7
|
+
* edge hides the answer. Region states are never reported: the graph does not
|
|
8
|
+
* record where a region starts.
|
|
9
|
+
*/
|
|
10
|
+
function unreachableStates(machine) {
|
|
11
|
+
const reached = new Set(machine.states.filter((state) => state.initial || state.region !== null).map((state) => state.id));
|
|
12
|
+
const queue = [...reached];
|
|
13
|
+
let undetermined = false;
|
|
14
|
+
for (let next = queue.shift(); next !== void 0; next = queue.shift()) {
|
|
15
|
+
const from = next;
|
|
16
|
+
for (const edge of machine.edges) {
|
|
17
|
+
if (edge.kind === "effect" || edge.from !== from && !coveredByAny(edge.from, from)) continue;
|
|
18
|
+
if (edge.to === null) {
|
|
19
|
+
undetermined = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (reached.has(edge.to)) continue;
|
|
23
|
+
reached.add(edge.to);
|
|
24
|
+
queue.push(edge.to);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (undetermined) return [];
|
|
28
|
+
return machine.states.map((state) => state.id).filter((id) => !reached.has(id));
|
|
29
|
+
}
|
|
30
|
+
/** Design errors of an inspected machine, one line each. Unreached edges and routing warnings are not errors. */
|
|
31
|
+
function designErrors(machine) {
|
|
32
|
+
const thrown = (machine.fuzz?.errors ?? []).map((error) => `${error.event} in ${error.from} throws: ${error.message}`);
|
|
33
|
+
const sends = machine.states.flatMap((state) => unhandledSends(machine, state.id).map(({ effect, event }) => `effect ${effect} in ${state.id} sends ${event}, but ${state.id} has no handler for it`));
|
|
34
|
+
const orphans = unreachableStates(machine).map((id) => `state ${id} is declared but no path leads to it from the initial state`);
|
|
35
|
+
return [
|
|
36
|
+
...thrown,
|
|
37
|
+
...sends,
|
|
38
|
+
...orphans
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Call from a test: throws when fuzzing finds a handler or effect that throws,
|
|
43
|
+
* an effect sends an event its state does not handle, or a declared state has
|
|
44
|
+
* no path from the initial state. Runs are seeded, so the result is stable.
|
|
45
|
+
*/
|
|
46
|
+
function expectNoDesignErrors(target) {
|
|
47
|
+
const { machine } = inspect(target);
|
|
48
|
+
const errors = designErrors(machine);
|
|
49
|
+
if (errors.length === 0) return;
|
|
50
|
+
throw new Error(`${machine.id} has ${errors.length} design error${errors.length === 1 ? "" : "s"}:\n${errors.map((each) => ` - ${each}`).join("\n")}`);
|
|
51
|
+
}
|
|
52
|
+
// src/reach.ts
|
|
53
|
+
function hitCount(coverage) {
|
|
54
|
+
return coverage?.hits ?? 0;
|
|
55
|
+
}
|
|
56
|
+
/** Ids of the states a fuzz run or the supplied test sequences visited. */
|
|
57
|
+
function reachedStates(machine) {
|
|
58
|
+
return machine.states.filter((state) => hitCount(state.coverage) + hitCount(state.tested) > 0).map((state) => state.id);
|
|
59
|
+
}
|
|
60
|
+
function isTaken(edge) {
|
|
61
|
+
return hitCount(edge.coverage) + hitCount(edge.tested) > 0;
|
|
62
|
+
}
|
|
63
|
+
function describeTransition({ from, event, to }) {
|
|
64
|
+
return to === void 0 ? `${from} --${event}-->` : `${from} --${event}--> ${to}`;
|
|
65
|
+
}
|
|
66
|
+
/** What the expectation asks for and the machine did not reach, one line each. */
|
|
67
|
+
function missedReach(machine, expected) {
|
|
68
|
+
const reached = new Set(reachedStates(machine));
|
|
69
|
+
const states = (expected.states ?? []).map((id) => {
|
|
70
|
+
if (!machine.states.some((state) => state.id === id)) return `state ${id} does not exist`;
|
|
71
|
+
return reached.has(id) ? null : `state ${id} was not reached`;
|
|
72
|
+
});
|
|
73
|
+
const transitions = (expected.transitions ?? []).map((wanted) => {
|
|
74
|
+
const matching = machine.edges.filter((edge) => edge.kind !== "effect" && edge.kind !== "initial" && edge.from === wanted.from && edge.event === wanted.event && (wanted.to === void 0 || edge.to === wanted.to));
|
|
75
|
+
if (matching.length === 0) return `transition ${describeTransition(wanted)} does not exist`;
|
|
76
|
+
return matching.some(isTaken) ? null : `transition ${describeTransition(wanted)} was not taken`;
|
|
77
|
+
});
|
|
78
|
+
return [...states, ...transitions].filter((line) => line !== null);
|
|
79
|
+
}
|
|
80
|
+
function failOnMiss(machine, misses) {
|
|
81
|
+
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
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Call from a test: throws unless fuzzing or the target's `tests` visited every
|
|
86
|
+
* listed state and took every listed transition. Reaching is proof; a miss only
|
|
87
|
+
* says nothing got there yet.
|
|
88
|
+
*/
|
|
89
|
+
function expectReached(target, expected) {
|
|
90
|
+
const { machine } = inspect(target);
|
|
91
|
+
failOnMiss(machine, missedReach(machine, expected));
|
|
92
|
+
}
|
|
93
|
+
/** Call from a test: throws unless fuzzing or the target's `tests` visited every declared state. */
|
|
94
|
+
function expectAllStatesReached(target) {
|
|
95
|
+
const { machine } = inspect(target);
|
|
96
|
+
failOnMiss(machine, missedReach(machine, { states: machine.states.map((state) => state.id) }));
|
|
97
|
+
}
|
|
98
|
+
export { analyzeModule, analyzeSource, annotateMachine, branchDistance, branchDistanceGuide, designErrors, eventCatalog, execute, expectAllStatesReached, expectNoDesignErrors, expectReached, fuzz, fuzzedMachine, guidance, inspect, inspectFile, inspectModule, literalPool, machineId, missedReach, reachedStates, shrink, toViz, toVizMachine, unreachableStates, vizDocument, withTestCoverage };
|
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
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
2
|
-
import { ANY_STATE,
|
|
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,
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { DEFAULT_SEED } from "@mantaq/pbt";
|
|
8
8
|
import { VirtualClock } from "@mantaq/core";
|
|
9
|
-
import { existsSync
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { loadConfigFromFile, loadEnv, runnerImport
|
|
11
|
+
import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
|
|
12
12
|
// src/structure.ts
|
|
13
13
|
function byId$1(left, right) {
|
|
14
14
|
if (left.id < right.id) return -1;
|
|
@@ -407,6 +407,7 @@ function fuzz(target, options) {
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
consider([], 0);
|
|
410
|
+
for (const steps of options.initial ?? []) consider([...steps], 0);
|
|
410
411
|
for (let iteration = 1; iteration <= options.iterations; iteration += 1) {
|
|
411
412
|
const at = context.rng.int(corpus.length);
|
|
412
413
|
consider(mutate({
|
|
@@ -1600,6 +1601,7 @@ function driven(target, context) {
|
|
|
1600
1601
|
const fuzzed = settings === false ? structure : fuzzedMachine(run[1], fuzz(run[1], {
|
|
1601
1602
|
seed: settings?.seed ?? DEFAULT_SEED,
|
|
1602
1603
|
iterations: settings?.iterations ?? DEFAULT_ITERATIONS,
|
|
1604
|
+
...target.tests === void 0 ? {} : { initial: target.tests },
|
|
1603
1605
|
...guidance(structure)
|
|
1604
1606
|
}));
|
|
1605
1607
|
return {
|
|
@@ -1858,155 +1860,4 @@ async function inspectFile(file, options = {}, load = {}) {
|
|
|
1858
1860
|
failed: false
|
|
1859
1861
|
};
|
|
1860
1862
|
}
|
|
1861
|
-
|
|
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 };
|
|
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 };
|
package/dist/vite.mjs
CHANGED
|
@@ -1 +1,159 @@
|
|
|
1
|
-
|
|
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";
|
|
2
|
+
import { UPDATE_EVENT, livePageHtml, serializeViz } from "@mantaq/viz";
|
|
3
|
+
import { Either } from "@mantaq/utils";
|
|
4
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
+
import { globSync, readFileSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { searchForWorkspaceRoot } from "vite";
|
|
8
|
+
// src/vite.ts
|
|
9
|
+
/**
|
|
10
|
+
* Vite plugin: a live page of your machines on your own dev server.
|
|
11
|
+
*
|
|
12
|
+
* // vite.config.ts
|
|
13
|
+
* import { mantaqViz } from "@mantaq/inspect/vite";
|
|
14
|
+
* export default defineConfig({ plugins: [mantaqViz({ machines: "src/**\/*.machine.ts" })] });
|
|
15
|
+
*
|
|
16
|
+
* Open `/__mantaq/` while the dev server runs. Machines load through your own
|
|
17
|
+
* Vite config (aliases, tsconfig paths), are inspected on the server, and the
|
|
18
|
+
* page redraws, keeping the walk, when you save a file.
|
|
19
|
+
*/
|
|
20
|
+
const STYLESHEETS = ["@mantaq/viz/viewer.css", "@mantaq/viz/page.css"];
|
|
21
|
+
const DEV_ITERATIONS = 500;
|
|
22
|
+
const TARGETS = "virtual:mantaq/targets";
|
|
23
|
+
const TARGETS_RESOLVED = `\0${TARGETS}`;
|
|
24
|
+
function createExpression(found, index) {
|
|
25
|
+
const callee = `m${index}[${JSON.stringify(found.name)}]`;
|
|
26
|
+
const call = found.call === "clock" ? `${callee}(VirtualClock())` : found.call === "options" ? `${callee}({ clock: VirtualClock() })` : `${callee}()`;
|
|
27
|
+
return found.unwrap === "actor" ? `${call}.actor` : call;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Source of `virtual:mantaq/targets`: one factory per exported actor factory,
|
|
31
|
+
* shaped by its type (`call`, `clock`, `options`). A bare actor export cannot
|
|
32
|
+
* be recreated, so it gets none. Like the server-side inspection, plain actors
|
|
33
|
+
* are skipped when a file has factories.
|
|
34
|
+
*/
|
|
35
|
+
function targetsSource(files) {
|
|
36
|
+
const core = fileURLToPath(import.meta.resolve("@mantaq/core"));
|
|
37
|
+
const imports = [];
|
|
38
|
+
const entries = [];
|
|
39
|
+
files.forEach((file, index) => {
|
|
40
|
+
const analysis = analyzeModule({
|
|
41
|
+
file,
|
|
42
|
+
root: projectRoot(file)
|
|
43
|
+
});
|
|
44
|
+
if (Either.isLeft(analysis)) return;
|
|
45
|
+
const factories = analysis[1].machines.filter((found) => found.call !== "instance");
|
|
46
|
+
if (factories.length === 0) return;
|
|
47
|
+
imports.push(`import * as m${index} from ${JSON.stringify(file)};`);
|
|
48
|
+
for (const found of factories) entries.push(` ${JSON.stringify(machineId(found.name))}: () => { const actor = ${createExpression(found, index)}; return { actor, clock: actor.clock }; },`);
|
|
49
|
+
});
|
|
50
|
+
return [
|
|
51
|
+
`import { VirtualClock } from ${JSON.stringify(core)};`,
|
|
52
|
+
...imports,
|
|
53
|
+
"export const targets = {",
|
|
54
|
+
...entries,
|
|
55
|
+
"};"
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
function machineFiles(root, patterns) {
|
|
59
|
+
const files = patterns.flatMap((pattern) => globSync(pattern, { cwd: root }));
|
|
60
|
+
return [...new Set(files.map((file) => isAbsolute(file) ? file : resolve(root, file)))].sort();
|
|
61
|
+
}
|
|
62
|
+
async function documentText(server, context) {
|
|
63
|
+
const results = [];
|
|
64
|
+
for (const file of context.files) {
|
|
65
|
+
const run = runOf(await server.ssrLoadModule(entryId(file)));
|
|
66
|
+
results.push(run === null ? {
|
|
67
|
+
machines: [],
|
|
68
|
+
warnings: [`${file}: could not load the inspector`]
|
|
69
|
+
} : await run({ fuzz: context.fuzz }));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
text: serializeViz(vizDocument(results.flatMap((result) => result.machines))),
|
|
73
|
+
warnings: results.flatMap((result) => result.warnings)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function mantaqViz(options) {
|
|
77
|
+
const base = (options.base ?? "/__mantaq/").replace(/\/?$/, "/");
|
|
78
|
+
const patterns = Array.isArray(options.machines) ? options.machines : [options.machines];
|
|
79
|
+
const fuzz = options.fuzz ?? { iterations: DEV_ITERATIONS };
|
|
80
|
+
const live = fileURLToPath(import.meta.resolve(import.meta.url.endsWith(".ts") ? "./live-entry.ts" : "./live-entry.mjs"));
|
|
81
|
+
const viz = dirname(dirname(fileURLToPath(import.meta.resolve("@mantaq/viz/live"))));
|
|
82
|
+
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
|
|
93
|
+
},
|
|
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
|
+
});
|
|
122
|
+
});
|
|
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);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
server.httpServer?.once("listening", () => {
|
|
153
|
+
server.config.logger.info(` mantaq viz: ${base}`);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
export { STYLESHEETS, TARGETS, TARGETS_RESOLVED, mantaqViz, targetsSource };
|
package/package.json
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/inspect",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Turn a Mantaq machine into a viz document: structure, handler explanations (AST), fuzz and test coverage.
|
|
3
|
+
"version": "0.4.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/AndersCan/mantaq",
|
|
9
9
|
"directory": "packages/inspect"
|
|
10
10
|
},
|
|
11
|
-
"bin": {
|
|
12
|
-
"mantaq-inspect": "./dist/cli.mjs"
|
|
13
|
-
},
|
|
14
11
|
"files": [
|
|
15
12
|
"dist"
|
|
16
13
|
],
|
|
@@ -18,7 +15,6 @@
|
|
|
18
15
|
"exports": {
|
|
19
16
|
".": "./dist/index.mjs",
|
|
20
17
|
"./drive": "./dist/drive.mjs",
|
|
21
|
-
"./live-embedded": "./dist/live-embedded.mjs",
|
|
22
18
|
"./live-entry": "./dist/live-entry.mjs",
|
|
23
19
|
"./vite": "./dist/vite.mjs",
|
|
24
20
|
"./package.json": "./package.json"
|
|
@@ -28,11 +24,11 @@
|
|
|
28
24
|
},
|
|
29
25
|
"dependencies": {
|
|
30
26
|
"typescript": "6.0.3",
|
|
31
|
-
"@mantaq/core": "0.5.
|
|
32
|
-
"@mantaq/traversal": "0.3.
|
|
33
|
-
"@mantaq/viz": "0.
|
|
34
|
-
"@mantaq/
|
|
35
|
-
"@mantaq/
|
|
27
|
+
"@mantaq/core": "0.5.4",
|
|
28
|
+
"@mantaq/traversal": "0.3.4",
|
|
29
|
+
"@mantaq/viz": "0.3.0",
|
|
30
|
+
"@mantaq/utils": "0.0.5",
|
|
31
|
+
"@mantaq/pbt": "0.0.4"
|
|
36
32
|
},
|
|
37
33
|
"devDependencies": {
|
|
38
34
|
"@types/node": "^26.2.0",
|
|
@@ -40,7 +36,7 @@
|
|
|
40
36
|
"happy-dom": "^21.0.4",
|
|
41
37
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
42
38
|
"vite-plus": "1.4.0",
|
|
43
|
-
"@mantaq/sugar": "2.0.
|
|
39
|
+
"@mantaq/sugar": "2.0.4"
|
|
44
40
|
},
|
|
45
41
|
"peerDependencies": {
|
|
46
42
|
"vite": "*"
|
package/dist/cli.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|
package/dist/cli.mjs
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { a as targetsSource, d as vizDocument, o as inlineConfig, r as TARGETS_RESOLVED, s as inspectFile } from "./vite-BBW0CmYr.mjs";
|
|
3
|
-
import { t as htmlPage } from "./page-CK7-IsLJ.mjs";
|
|
4
|
-
import { serializeViz } from "@mantaq/viz";
|
|
5
|
-
import { Either } from "@mantaq/utils";
|
|
6
|
-
import { basename, resolve } from "node:path";
|
|
7
|
-
import { SEED_ENV, parseSeed } from "@mantaq/pbt";
|
|
8
|
-
import { writeFileSync } from "node:fs";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { build } from "vite";
|
|
11
|
-
import { spawn } from "node:child_process";
|
|
12
|
-
import { parseArgs } from "node:util";
|
|
13
|
-
// src/live-html.ts
|
|
14
|
-
/**
|
|
15
|
-
* The live page as one classic script: the viewer, the driver and every
|
|
16
|
-
* exported factory of `files`, built with the project's Vite config.
|
|
17
|
-
*/
|
|
18
|
-
async function liveBundle(files, load) {
|
|
19
|
-
const entry = fileURLToPath(import.meta.resolve(import.meta.url.endsWith(".ts") ? "./live-embedded.ts" : "./live-embedded.mjs"));
|
|
20
|
-
const targets = {
|
|
21
|
-
name: "mantaq-live-targets",
|
|
22
|
-
resolveId: (id) => id === "virtual:mantaq/targets" ? TARGETS_RESOLVED : null,
|
|
23
|
-
load: (id) => id === "\0virtual:mantaq/targets" ? targetsSource(files) : null
|
|
24
|
-
};
|
|
25
|
-
const inline = await inlineConfig(load);
|
|
26
|
-
const output = await build({
|
|
27
|
-
...inline,
|
|
28
|
-
logLevel: "silent",
|
|
29
|
-
plugins: [...inline.plugins ?? [], targets],
|
|
30
|
-
build: {
|
|
31
|
-
write: false,
|
|
32
|
-
minify: true,
|
|
33
|
-
cssCodeSplit: false,
|
|
34
|
-
lib: {
|
|
35
|
-
entry,
|
|
36
|
-
formats: ["iife"],
|
|
37
|
-
name: "MantaqLive",
|
|
38
|
-
fileName: () => "live.js"
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
return (Array.isArray(output) ? output : [output]).flatMap((each) => "output" in each ? each.output : []).flatMap((file) => file.type === "chunk" ? [file.code] : []).join("\n");
|
|
43
|
-
}
|
|
44
|
-
// src/cli.ts
|
|
45
|
-
/**
|
|
46
|
-
* mantaq-inspect: draw the machines a file exports.
|
|
47
|
-
*
|
|
48
|
-
* mantaq-inspect <file.ts>... [--out FILE] [--json] [--live] [--no-fuzz] [--iterations N] [--config FILE]
|
|
49
|
-
* [--mode MODE] [--no-open] [--check]
|
|
50
|
-
*
|
|
51
|
-
* Finds every exported actor or actor factory from the file's types, reads
|
|
52
|
-
* handler explanations and sample payloads from the source, fuzzes each
|
|
53
|
-
* machine that runs on a VirtualClock, and writes one self-contained HTML page
|
|
54
|
-
* (default `mantaq-viz.html`) that opens without a server. `--json` writes the
|
|
55
|
-
* viz document instead. `--live` bundles the machine files into the page, so its
|
|
56
|
-
* steps run the real actors (factories on a VirtualClock) with no server. `--config` loads the files with that Vite config (env,
|
|
57
|
-
* aliases, plugins) and `--mode` picks its mode. `--check` exits 1 if any warning was printed or
|
|
58
|
-
* fuzzing found a handler error (each printed as `error:`). Seed: MANTAQ_SEED (default from @mantaq/pbt).
|
|
59
|
-
*/
|
|
60
|
-
const USAGE = "usage: mantaq-inspect <file.ts>... [--out FILE] [--json] [--live] [--no-fuzz] [--iterations N] [--config FILE] [--mode MODE] [--no-open] [--check]\n";
|
|
61
|
-
function loadOptions(config, mode) {
|
|
62
|
-
return {
|
|
63
|
-
...config === void 0 ? {} : { config: resolve(config) },
|
|
64
|
-
...mode === void 0 ? {} : { mode }
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function fuzzSettings(iterationsArg, off) {
|
|
68
|
-
const iterations = Number(iterationsArg ?? 2e3);
|
|
69
|
-
if (!Number.isInteger(iterations) || iterations < 0) return Either.left("bad --iterations");
|
|
70
|
-
const seed = parseSeed(process.env[SEED_ENV]);
|
|
71
|
-
if (Either.isLeft(seed)) return Either.left(seed[0].message);
|
|
72
|
-
const settings = off ? false : {
|
|
73
|
-
seed: seed[1],
|
|
74
|
-
iterations
|
|
75
|
-
};
|
|
76
|
-
return Either.right(settings);
|
|
77
|
-
}
|
|
78
|
-
function parse() {
|
|
79
|
-
const parsed = Either.from(() => parseArgs({
|
|
80
|
-
allowPositionals: true,
|
|
81
|
-
options: {
|
|
82
|
-
out: { type: "string" },
|
|
83
|
-
json: { type: "boolean" },
|
|
84
|
-
live: { type: "boolean" },
|
|
85
|
-
iterations: { type: "string" },
|
|
86
|
-
config: { type: "string" },
|
|
87
|
-
mode: { type: "string" },
|
|
88
|
-
"no-fuzz": { type: "boolean" },
|
|
89
|
-
"no-open": { type: "boolean" },
|
|
90
|
-
check: { type: "boolean" }
|
|
91
|
-
}
|
|
92
|
-
}));
|
|
93
|
-
if (Either.isLeft(parsed)) return Either.left(String(parsed[0]));
|
|
94
|
-
const { positionals, values } = parsed[1];
|
|
95
|
-
if (positionals.length === 0) return Either.left("no file given");
|
|
96
|
-
const json = values.json === true;
|
|
97
|
-
const live = values.live === true;
|
|
98
|
-
if (live && json) return Either.left("--live and --json cannot be combined");
|
|
99
|
-
const fuzz = fuzzSettings(values.iterations, values["no-fuzz"] === true);
|
|
100
|
-
if (Either.isLeft(fuzz)) return Either.left(fuzz[0]);
|
|
101
|
-
return Either.right({
|
|
102
|
-
files: positionals.map((file) => resolve(file)),
|
|
103
|
-
out: resolve(values.out ?? (json ? "mantaq-viz.json" : "mantaq-viz.html")),
|
|
104
|
-
json,
|
|
105
|
-
live,
|
|
106
|
-
fuzz: fuzz[1],
|
|
107
|
-
check: values.check === true,
|
|
108
|
-
load: loadOptions(values.config, values.mode),
|
|
109
|
-
open: values["no-open"] !== true && !json && process.stdout.isTTY === true
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function openInBrowser(path) {
|
|
113
|
-
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
114
|
-
spawn(command, [path], {
|
|
115
|
-
stdio: "ignore",
|
|
116
|
-
detached: true
|
|
117
|
-
}).on("error", () => {}).unref();
|
|
118
|
-
}
|
|
119
|
-
function checkFails(results, machines) {
|
|
120
|
-
const errors = machines.flatMap((machine) => (machine.fuzz?.errors ?? []).map((error) => `${machine.id}: ${error.from} -${error.event}-> ${error.message}`));
|
|
121
|
-
for (const error of errors) process.stderr.write(`error: ${error}\n`);
|
|
122
|
-
return errors.length > 0 || results.some((result) => result.warnings.length > 0);
|
|
123
|
-
}
|
|
124
|
-
async function main() {
|
|
125
|
-
const options = parse();
|
|
126
|
-
if (Either.isLeft(options)) {
|
|
127
|
-
process.stderr.write(`${options[0]}\n${USAGE}`);
|
|
128
|
-
process.exitCode = 1;
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
const { files, out, json, live, fuzz, open, check, load } = options[1];
|
|
132
|
-
const results = [];
|
|
133
|
-
for (const file of files) results.push(await inspectFile(file, { fuzz }, load));
|
|
134
|
-
for (const warning of results.flatMap((result) => result.warnings)) process.stderr.write(`warning: ${warning}\n`);
|
|
135
|
-
const failed = results.some((result) => result.failed);
|
|
136
|
-
if (failed) process.exitCode = 1;
|
|
137
|
-
const machines = results.flatMap((result) => result.machines);
|
|
138
|
-
if (check && checkFails(results, machines)) process.exitCode = 1;
|
|
139
|
-
if (machines.length === 0) {
|
|
140
|
-
if (!failed) process.stderr.write("no machines found\n");
|
|
141
|
-
process.exitCode = 1;
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
const document = vizDocument(machines);
|
|
145
|
-
const title = `${files.map((file) => basename(file)).join(", ")} · mantaq viz`;
|
|
146
|
-
const liveScript = live ? await liveBundle(files, load) : void 0;
|
|
147
|
-
writeFileSync(out, json ? serializeViz(document) : await htmlPage(document, title, liveScript));
|
|
148
|
-
process.stdout.write(`wrote ${out} (${machines.map((machine) => machine.id).join(", ")})\n`);
|
|
149
|
-
if (open) openInBrowser(out);
|
|
150
|
-
}
|
|
151
|
-
main();
|
|
152
|
-
export {};
|
package/dist/live-embedded.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|
package/dist/live-embedded.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { t as mountDriven } from "./live-page-DEDl_Nio.mjs";
|
|
2
|
-
import { DOCUMENT_ELEMENT_ID } from "@mantaq/viz";
|
|
3
|
-
import { targets } from "virtual:mantaq/targets";
|
|
4
|
-
// src/live-embedded.ts
|
|
5
|
-
/**
|
|
6
|
-
* Entry of the standalone live page `mantaq-inspect --live` writes: the viz
|
|
7
|
-
* document sits in the page, the machines are bundled in, nothing is fetched.
|
|
8
|
-
*/
|
|
9
|
-
const root = document.querySelector("#app");
|
|
10
|
-
const text = document.getElementById(DOCUMENT_ELEMENT_ID)?.textContent;
|
|
11
|
-
if (root && text) mountDriven(root, {
|
|
12
|
-
source: "this page",
|
|
13
|
-
text,
|
|
14
|
-
targets
|
|
15
|
-
});
|
|
16
|
-
export {};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { createDriver } from "./drive.mjs";
|
|
2
|
-
import { parseViz } from "@mantaq/viz";
|
|
3
|
-
import { Either } from "@mantaq/utils";
|
|
4
|
-
import { mountViewer, showIssues } from "@mantaq/viz/viewer";
|
|
5
|
-
import { linkOptions } from "@mantaq/viz/page";
|
|
6
|
-
// src/live-page.ts
|
|
7
|
-
/**
|
|
8
|
-
* The live page of the Vite plugin: the served document, with each machine
|
|
9
|
-
* that has a target running for real behind the viewer. The walk lives in the
|
|
10
|
-
* address bar (`?machine=&step=`), so a redraw after a save replays it on the
|
|
11
|
-
* new code.
|
|
12
|
-
*/
|
|
13
|
-
/** A document holding only `machine`, so the viewer shows the machine its driver runs. */
|
|
14
|
-
function only(viz, machine) {
|
|
15
|
-
const chosen = viz.machines.find((each) => each.id === machine) ?? viz.machines[0];
|
|
16
|
-
return {
|
|
17
|
-
...viz,
|
|
18
|
-
machines: chosen === void 0 ? [] : [chosen]
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
/** Mount the document at `text`; machines with a target run live, the rest stay static walks. */
|
|
22
|
-
function mountDriven(root, context) {
|
|
23
|
-
const parsed = parseViz(context.text);
|
|
24
|
-
if (Either.isLeft(parsed)) {
|
|
25
|
-
showIssues(root, {
|
|
26
|
-
source: context.source,
|
|
27
|
-
issues: parsed[0]
|
|
28
|
-
});
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const options = linkOptions();
|
|
32
|
-
const viz = parsed[1];
|
|
33
|
-
const shown = only(viz, options.machine ?? null);
|
|
34
|
-
const id = shown.machines[0]?.id ?? "";
|
|
35
|
-
const target = context.targets[id];
|
|
36
|
-
const create = target !== void 0 && typeof Reflect.get(target().clock, "advance") === "function" ? target : void 0;
|
|
37
|
-
const picker = viz.machines.length > 1;
|
|
38
|
-
const select = (machine) => {
|
|
39
|
-
const next = new URLSearchParams(location.search);
|
|
40
|
-
next.set("machine", machine);
|
|
41
|
-
next.delete("step");
|
|
42
|
-
history.replaceState(null, "", `?${next.toString()}`);
|
|
43
|
-
mountDriven(root, context);
|
|
44
|
-
};
|
|
45
|
-
const doc = root.ownerDocument;
|
|
46
|
-
const bar = doc.createElement("nav");
|
|
47
|
-
bar.className = "machines";
|
|
48
|
-
if (picker) for (const machine of viz.machines) {
|
|
49
|
-
const button = doc.createElement("button");
|
|
50
|
-
button.type = "button";
|
|
51
|
-
button.textContent = machine.id;
|
|
52
|
-
button.disabled = machine.id === id;
|
|
53
|
-
button.addEventListener("click", () => select(machine.id));
|
|
54
|
-
bar.append(button);
|
|
55
|
-
}
|
|
56
|
-
const viewer = doc.createElement("div");
|
|
57
|
-
root.replaceChildren(...picker ? [bar] : [], viewer);
|
|
58
|
-
mountViewer(viewer, shown, {
|
|
59
|
-
...options,
|
|
60
|
-
...create === void 0 ? {} : { driver: createDriver({
|
|
61
|
-
id,
|
|
62
|
-
create
|
|
63
|
-
}) }
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/** Fetch the document from `source` and mount it; call again to redraw, keeping the walk. */
|
|
67
|
-
async function mountLiveDriven(root, context) {
|
|
68
|
-
const response = await fetch(context.source).catch(() => null);
|
|
69
|
-
if (response === null || !response.ok) {
|
|
70
|
-
const status = response === null ? "network error" : `HTTP ${response.status}`;
|
|
71
|
-
showIssues(root, {
|
|
72
|
-
source: context.source,
|
|
73
|
-
issues: [{
|
|
74
|
-
path: "$",
|
|
75
|
-
message: status
|
|
76
|
-
}]
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
mountDriven(root, {
|
|
81
|
-
...context,
|
|
82
|
-
text: await response.text()
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
export { mountLiveDriven as n, mountDriven as t };
|
package/dist/page-CK7-IsLJ.mjs
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { t as STYLESHEETS } from "./vite-BBW0CmYr.mjs";
|
|
2
|
-
import { serializeViz, standaloneHtml } from "@mantaq/viz";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { build } from "vite";
|
|
6
|
-
// src/page.ts
|
|
7
|
-
/** The viewer as one classic script, built from `@mantaq/viz/standalone`. */
|
|
8
|
-
async function viewerScript() {
|
|
9
|
-
const entry = fileURLToPath(import.meta.resolve("@mantaq/viz/standalone"));
|
|
10
|
-
const output = await build({
|
|
11
|
-
configFile: false,
|
|
12
|
-
logLevel: "silent",
|
|
13
|
-
build: {
|
|
14
|
-
write: false,
|
|
15
|
-
minify: true,
|
|
16
|
-
cssCodeSplit: false,
|
|
17
|
-
lib: {
|
|
18
|
-
entry,
|
|
19
|
-
formats: ["iife"],
|
|
20
|
-
name: "MantaqViz",
|
|
21
|
-
fileName: () => "viz.js"
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
return (Array.isArray(output) ? output : [output]).flatMap((each) => "output" in each ? each.output : []).flatMap((file) => file.type === "chunk" ? [file.code] : []).join("\n");
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* A self-contained HTML page for `document`: open it from disk, attach it, commit it.
|
|
29
|
-
* With `live` (a built script that drives the machines), that script replaces the static viewer.
|
|
30
|
-
*/
|
|
31
|
-
async function htmlPage(document, title, live) {
|
|
32
|
-
const css = STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n");
|
|
33
|
-
const script = live ?? await viewerScript();
|
|
34
|
-
return standaloneHtml({
|
|
35
|
-
title,
|
|
36
|
-
script,
|
|
37
|
-
css,
|
|
38
|
-
document: serializeViz(document)
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
export { htmlPage as t };
|