@mantaq/inspect 0.1.0 → 0.2.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 +2 -1
- package/dist/cli.mjs +30 -8
- package/dist/drive.d.mts +7 -0
- package/dist/drive.mjs +92 -0
- package/dist/index.d.mts +17 -60
- package/dist/index.mjs +2 -1
- package/dist/live-entry.d.mts +1 -0
- package/dist/live-entry.mjs +106 -0
- package/dist/{load-C4rGWmGU.mjs → load-DOwsxTyL.mjs} +191 -141
- package/dist/run-B5huYj_r.mjs +117 -0
- package/dist/run-BLt0mwXe.d.mts +60 -0
- package/dist/vite.mjs +111 -54
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ It finds every exported actor, or function that builds one, from the file's type
|
|
|
10
10
|
|
|
11
11
|
## Three ways in
|
|
12
12
|
|
|
13
|
-
**CLI**: `mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--no-open]`. `--json` writes the viz document instead of the page. Seed: `MANTAQ_SEED` (default from `@mantaq/pbt`). Same seed → same output.
|
|
13
|
+
**CLI**: `mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--config FILE] [--mode MODE] [--no-open] [--check]`. `--config` loads the files with that Vite config (env, aliases, plugins) and `--mode` picks its mode; without `--config` no project config is read. `--check` exits 1 if a warning was printed or fuzzing found a handler error (printed as `error:`), so CI can gate on design errors. `--json` writes the viz document instead of the page. Seed: `MANTAQ_SEED` (default from `@mantaq/pbt`). Same seed → same output.
|
|
14
14
|
|
|
15
15
|
**Vite plugin**: a live page on your dev server that redraws, keeping your walk, when you save.
|
|
16
16
|
|
|
@@ -47,6 +47,7 @@ const json = serializeViz(vizDocument([machine]));
|
|
|
47
47
|
|
|
48
48
|
- **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.
|
|
49
49
|
- **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.
|
|
50
|
+
- **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/).
|
|
50
51
|
- **A `VirtualClock`**, to fuzz. An actor on the real clock is drawn, with a warning.
|
|
51
52
|
|
|
52
53
|
Every part that cannot run is skipped with a warning, never guessed.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { l as vizDocument, r as inspectFile } from "./load-DOwsxTyL.mjs";
|
|
3
3
|
import { t as htmlPage } from "./page-rr9IQ-4k.mjs";
|
|
4
4
|
import { serializeViz } from "@mantaq/viz";
|
|
5
5
|
import { Either } from "@mantaq/utils";
|
|
@@ -12,15 +12,24 @@ import { parseArgs } from "node:util";
|
|
|
12
12
|
/**
|
|
13
13
|
* mantaq-inspect: draw the machines a file exports.
|
|
14
14
|
*
|
|
15
|
-
* mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--
|
|
15
|
+
* mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--config FILE]
|
|
16
|
+
* [--mode MODE] [--no-open] [--check]
|
|
16
17
|
*
|
|
17
18
|
* Finds every exported actor or actor factory from the file's types, reads
|
|
18
19
|
* handler explanations and sample payloads from the source, fuzzes each
|
|
19
20
|
* machine that runs on a VirtualClock, and writes one self-contained HTML page
|
|
20
21
|
* (default `mantaq-viz.html`) that opens without a server. `--json` writes the
|
|
21
|
-
* viz document instead.
|
|
22
|
+
* viz document instead. `--config` loads the files with that Vite config (env,
|
|
23
|
+
* aliases, plugins) and `--mode` picks its mode. `--check` exits 1 if any warning was printed or
|
|
24
|
+
* fuzzing found a handler error (each printed as `error:`). Seed: MANTAQ_SEED (default from @mantaq/pbt).
|
|
22
25
|
*/
|
|
23
|
-
const USAGE = "usage: mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--no-open]\n";
|
|
26
|
+
const USAGE = "usage: mantaq-inspect <file.ts>... [--out FILE] [--json] [--no-fuzz] [--iterations N] [--config FILE] [--mode MODE] [--no-open] [--check]\n";
|
|
27
|
+
function loadOptions(config, mode) {
|
|
28
|
+
return {
|
|
29
|
+
...config === void 0 ? {} : { config: resolve(config) },
|
|
30
|
+
...mode === void 0 ? {} : { mode }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
24
33
|
function parse() {
|
|
25
34
|
const parsed = Either.from(() => parseArgs({
|
|
26
35
|
allowPositionals: true,
|
|
@@ -28,8 +37,11 @@ function parse() {
|
|
|
28
37
|
out: { type: "string" },
|
|
29
38
|
json: { type: "boolean" },
|
|
30
39
|
iterations: { type: "string" },
|
|
40
|
+
config: { type: "string" },
|
|
41
|
+
mode: { type: "string" },
|
|
31
42
|
"no-fuzz": { type: "boolean" },
|
|
32
|
-
"no-open": { type: "boolean" }
|
|
43
|
+
"no-open": { type: "boolean" },
|
|
44
|
+
check: { type: "boolean" }
|
|
33
45
|
}
|
|
34
46
|
}));
|
|
35
47
|
if (Either.isLeft(parsed)) return Either.left(String(parsed[0]));
|
|
@@ -49,6 +61,8 @@ function parse() {
|
|
|
49
61
|
out: resolve(values.out ?? (json ? "mantaq-viz.json" : "mantaq-viz.html")),
|
|
50
62
|
json,
|
|
51
63
|
fuzz,
|
|
64
|
+
check: values.check === true,
|
|
65
|
+
load: loadOptions(values.config, values.mode),
|
|
52
66
|
open: values["no-open"] !== true && !json && process.stdout.isTTY === true
|
|
53
67
|
});
|
|
54
68
|
}
|
|
@@ -59,6 +73,11 @@ function openInBrowser(path) {
|
|
|
59
73
|
detached: true
|
|
60
74
|
}).on("error", () => {}).unref();
|
|
61
75
|
}
|
|
76
|
+
function checkFails(results, machines) {
|
|
77
|
+
const errors = machines.flatMap((machine) => (machine.fuzz?.errors ?? []).map((error) => `${machine.id}: ${error.from} -${error.event}-> ${error.message}`));
|
|
78
|
+
for (const error of errors) process.stderr.write(`error: ${error}\n`);
|
|
79
|
+
return errors.length > 0 || results.some((result) => result.warnings.length > 0);
|
|
80
|
+
}
|
|
62
81
|
async function main() {
|
|
63
82
|
const options = parse();
|
|
64
83
|
if (Either.isLeft(options)) {
|
|
@@ -66,13 +85,16 @@ async function main() {
|
|
|
66
85
|
process.exitCode = 1;
|
|
67
86
|
return;
|
|
68
87
|
}
|
|
69
|
-
const { files, out, json, fuzz, open } = options[1];
|
|
88
|
+
const { files, out, json, fuzz, open, check, load } = options[1];
|
|
70
89
|
const results = [];
|
|
71
|
-
for (const file of files) results.push(await inspectFile(file, { fuzz }));
|
|
90
|
+
for (const file of files) results.push(await inspectFile(file, { fuzz }, load));
|
|
72
91
|
for (const warning of results.flatMap((result) => result.warnings)) process.stderr.write(`warning: ${warning}\n`);
|
|
92
|
+
const failed = results.some((result) => result.failed);
|
|
93
|
+
if (failed) process.exitCode = 1;
|
|
73
94
|
const machines = results.flatMap((result) => result.machines);
|
|
95
|
+
if (check && checkFails(results, machines)) process.exitCode = 1;
|
|
74
96
|
if (machines.length === 0) {
|
|
75
|
-
process.stderr.write("no machines found\n");
|
|
97
|
+
if (!failed) process.stderr.write("no machines found\n");
|
|
76
98
|
process.exitCode = 1;
|
|
77
99
|
return;
|
|
78
100
|
}
|
package/dist/drive.d.mts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { t as FuzzTarget } from "./run-BLt0mwXe.mjs";
|
|
2
|
+
import { VizDriver } from "@mantaq/viz";
|
|
3
|
+
/**
|
|
4
|
+
* A running actor for the viewer. Steps go through the same function the
|
|
5
|
+
* fuzzer replays with, so live and fuzzed behaviour cannot drift. No node APIs.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createDriver(target: FuzzTarget): VizDriver;
|
package/dist/drive.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { a as messageOf, n as contextText, o as parsed, r as eventCatalog, t as applyStep } from "./run-B5huYj_r.mjs";
|
|
2
|
+
// src/drive.ts
|
|
3
|
+
function json(text) {
|
|
4
|
+
return parsed(text);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A running actor for the viewer. Steps go through the same function the
|
|
8
|
+
* fuzzer replays with, so live and fuzzed behaviour cannot drift. No node APIs.
|
|
9
|
+
*/
|
|
10
|
+
function createDriver(target) {
|
|
11
|
+
let session = start(target);
|
|
12
|
+
function start(from) {
|
|
13
|
+
const { actor, clock } = from.create();
|
|
14
|
+
const internal = new Set(eventCatalog(actor).internal);
|
|
15
|
+
const seen = { last: null };
|
|
16
|
+
const steps = [];
|
|
17
|
+
actor.on("transition", { fn: ({ event, from: before, to, transitioned, effects }) => {
|
|
18
|
+
seen.last = {
|
|
19
|
+
from: before,
|
|
20
|
+
event: event.type,
|
|
21
|
+
to,
|
|
22
|
+
outcome: transitioned ? "transition" : "stay",
|
|
23
|
+
effects,
|
|
24
|
+
context: json(contextText(actor)),
|
|
25
|
+
error: null
|
|
26
|
+
};
|
|
27
|
+
} });
|
|
28
|
+
actor.on("error", { fn: (info) => {
|
|
29
|
+
const error = messageOf(info.error);
|
|
30
|
+
seen.last = {
|
|
31
|
+
from: info.state.name,
|
|
32
|
+
event: info.event.type,
|
|
33
|
+
to: actor.state.name,
|
|
34
|
+
outcome: "stay",
|
|
35
|
+
effects: [],
|
|
36
|
+
context: json(contextText(actor)),
|
|
37
|
+
error
|
|
38
|
+
};
|
|
39
|
+
} });
|
|
40
|
+
return {
|
|
41
|
+
actor,
|
|
42
|
+
clock,
|
|
43
|
+
internal,
|
|
44
|
+
seen,
|
|
45
|
+
steps
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function view() {
|
|
49
|
+
const { actor, clock, steps, seen } = session;
|
|
50
|
+
return {
|
|
51
|
+
state: actor.state.name,
|
|
52
|
+
steps: [...steps],
|
|
53
|
+
last: seen.last,
|
|
54
|
+
timers: clock.pendingTimers().map((timer) => ({
|
|
55
|
+
id: String(timer.id),
|
|
56
|
+
ms: timer.ms,
|
|
57
|
+
deadline: timer.deadline,
|
|
58
|
+
eventName: timer.eventName ?? null
|
|
59
|
+
})),
|
|
60
|
+
context: json(contextText(actor))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
reset() {
|
|
65
|
+
session.actor.dispose();
|
|
66
|
+
session = start(target);
|
|
67
|
+
return view();
|
|
68
|
+
},
|
|
69
|
+
step(step) {
|
|
70
|
+
const { actor, clock, internal, seen, steps } = session;
|
|
71
|
+
seen.last = null;
|
|
72
|
+
const before = actor.state.name;
|
|
73
|
+
applyStep(actor, {
|
|
74
|
+
step,
|
|
75
|
+
clock,
|
|
76
|
+
internal
|
|
77
|
+
});
|
|
78
|
+
steps.push(step);
|
|
79
|
+
if (seen.last === null && step.kind === "event") seen.last = {
|
|
80
|
+
from: before,
|
|
81
|
+
event: step.event,
|
|
82
|
+
to: before,
|
|
83
|
+
outcome: "ignored",
|
|
84
|
+
effects: [],
|
|
85
|
+
context: json(contextText(actor)),
|
|
86
|
+
error: null
|
|
87
|
+
};
|
|
88
|
+
return view();
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export { createDriver };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { a as eventCatalog, i as Trace, n as Observation, o as execute, r as RunError, t as FuzzTarget } from "./run-BLt0mwXe.mjs";
|
|
1
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
3
|
import { VizComparison, VizDocument, VizExplanation, VizFuzzError, VizJson, VizMachine, VizStep } from "@mantaq/viz";
|
|
3
4
|
import { History } from "@mantaq/traversal";
|
|
4
5
|
import { Either } from "@mantaq/utils";
|
|
5
|
-
import { AnyActor
|
|
6
|
+
import { AnyActor } from "@mantaq/core";
|
|
6
7
|
import { Plugin } from "vite";
|
|
7
8
|
interface VizInput {
|
|
8
9
|
id: string;
|
|
@@ -23,63 +24,6 @@ interface LiteralPool {
|
|
|
23
24
|
numbers: number[];
|
|
24
25
|
strings: string[];
|
|
25
26
|
}
|
|
26
|
-
/** A machine the fuzzer can drive: a fresh actor plus the virtual clock it runs on. */
|
|
27
|
-
interface FuzzTarget {
|
|
28
|
-
id: string;
|
|
29
|
-
create(): {
|
|
30
|
-
actor: AnyActor<unknown>;
|
|
31
|
-
clock: VirtualClock;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Sample payloads per event type. Payload types are erased at runtime, so
|
|
35
|
-
* the fuzzer only mutates shapes it was given. Events without samples are
|
|
36
|
-
* sent without a payload.
|
|
37
|
-
*/
|
|
38
|
-
payloads?: Readonly<Record<string, ReadonlyArray<VizJson>>>;
|
|
39
|
-
}
|
|
40
|
-
interface Observation {
|
|
41
|
-
from: string;
|
|
42
|
-
event: string;
|
|
43
|
-
to: string;
|
|
44
|
-
transitioned: boolean;
|
|
45
|
-
/** Names of the effects that ran on entry into `to`. */
|
|
46
|
-
effects: string[];
|
|
47
|
-
/** Index of the step that caused it. */
|
|
48
|
-
step: number;
|
|
49
|
-
/** Payload the handler received (for branch-distance guidance). */
|
|
50
|
-
payload: unknown;
|
|
51
|
-
/** JSON copy of the actor context right after the handler ran; null if not JSON. */
|
|
52
|
-
context: unknown;
|
|
53
|
-
}
|
|
54
|
-
interface RunError {
|
|
55
|
-
from: string;
|
|
56
|
-
event: string;
|
|
57
|
-
message: string;
|
|
58
|
-
/** Index of the step that triggered it. */
|
|
59
|
-
step: number;
|
|
60
|
-
}
|
|
61
|
-
interface Trace {
|
|
62
|
-
visited: string[];
|
|
63
|
-
observations: Observation[];
|
|
64
|
-
errors: RunError[];
|
|
65
|
-
/** Indices of steps after which the actor context differed: loop candidates. */
|
|
66
|
-
contextSteps: number[];
|
|
67
|
-
/**
|
|
68
|
-
* Coverage features, sorted: `state:x`, `edge:a>e>b` (state changed or
|
|
69
|
-
* re-entered), `stay:a>e` (handled, no change), `error:a>e` (handler threw).
|
|
70
|
-
*/
|
|
71
|
-
features: string[];
|
|
72
|
-
}
|
|
73
|
-
/** Declared inputs and internal events, each sorted. */
|
|
74
|
-
export declare function eventCatalog(actor: AnyActor<unknown>): {
|
|
75
|
-
inputs: string[];
|
|
76
|
-
internal: string[];
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Replay steps against a fresh actor. Synchronous and deterministic: effects
|
|
80
|
-
* only run via the virtual clock, and nothing here reads wall time.
|
|
81
|
-
*/
|
|
82
|
-
export declare function execute(target: FuzzTarget, steps: ReadonlyArray<VizStep>): Trace;
|
|
83
27
|
interface FuzzOptions {
|
|
84
28
|
seed: number;
|
|
85
29
|
iterations: number;
|
|
@@ -169,12 +113,16 @@ interface HandlerSite {
|
|
|
169
113
|
state: string | null;
|
|
170
114
|
event: string;
|
|
171
115
|
explanation: VizExplanation;
|
|
116
|
+
/** Leading JSDoc of the registration. null = undocumented. */
|
|
117
|
+
doc: string | null;
|
|
172
118
|
}
|
|
173
119
|
/** Explain every handler registration in a file. Left = the file could not be analyzed at all. */
|
|
174
120
|
export declare function analyzeSource(source: AnalysisSource): Either<string, HandlerSite[]>;
|
|
175
121
|
/** Everything one type-check of a machine file yields. */
|
|
176
122
|
interface ModuleAnalysis {
|
|
177
123
|
sites: HandlerSite[];
|
|
124
|
+
stateDocs: ReadonlyMap<string, string>;
|
|
125
|
+
eventDocs: ReadonlyMap<string, string>;
|
|
178
126
|
payloads: Record<string, VizJson[]>;
|
|
179
127
|
machines: MachineExport[];
|
|
180
128
|
}
|
|
@@ -188,8 +136,17 @@ export declare function analyzeModule(source: AnalysisSource): Either<string, Mo
|
|
|
188
136
|
export declare function annotateMachine(machine: VizMachine, source: AnalysisSource): VizMachine;
|
|
189
137
|
/** Options `inspectFile` passes through to `inspectModule`; loading is its job. */
|
|
190
138
|
type FileOptions = Omit<ModuleOptions, "load">;
|
|
139
|
+
/** An inspected file; `failed` when it could not be loaded at all (its warnings say why). */
|
|
140
|
+
type LoadedFile = InspectedModule & {
|
|
141
|
+
failed: boolean;
|
|
142
|
+
};
|
|
143
|
+
/** Which Vite config and mode load the file; without a config, no project config is read. */
|
|
144
|
+
interface LoadOptions {
|
|
145
|
+
config?: string;
|
|
146
|
+
mode?: string;
|
|
147
|
+
}
|
|
191
148
|
/** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
|
|
192
|
-
export declare function inspectFile(file: string, options?: FileOptions): Promise<
|
|
149
|
+
export declare function inspectFile(file: string, options?: FileOptions, load?: LoadOptions): Promise<LoadedFile>;
|
|
193
150
|
/** A self-contained HTML page for `document`: open it from disk, attach it, commit it. */
|
|
194
151
|
export declare function htmlPage(document: VizDocument, title: string): Promise<string>;
|
|
195
|
-
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 RunError, type Trace, type VizInput, inspect, inspectModule, machineId, vizDocument };
|
|
152
|
+
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 RunError, type Trace, type VizInput, eventCatalog, execute, inspect, inspectModule, machineId, vizDocument };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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-DOwsxTyL.mjs";
|
|
2
|
+
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
2
3
|
import { t as htmlPage } from "./page-rr9IQ-4k.mjs";
|
|
3
4
|
export { analyzeModule, analyzeSource, annotateMachine, branchDistance, branchDistanceGuide, eventCatalog, execute, fuzz, fuzzedMachine, guidance, htmlPage, inspect, inspectFile, inspectModule, literalPool, machineId, shrink, toViz, toVizMachine, vizDocument, withTestCoverage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createDriver } from "./drive.mjs";
|
|
2
|
+
import { UPDATE_EVENT, parseViz } from "@mantaq/viz";
|
|
3
|
+
import { Either } from "@mantaq/utils";
|
|
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
|
+
}
|
|
86
|
+
// src/live-entry.ts
|
|
87
|
+
/**
|
|
88
|
+
* Entry of the page `@mantaq/inspect/vite` serves. Redraws when the dev
|
|
89
|
+
* server reports a change, and when the machine code itself hot-updates.
|
|
90
|
+
*/
|
|
91
|
+
const root = document.querySelector("#app");
|
|
92
|
+
const source = root?.dataset["src"];
|
|
93
|
+
const current = { targets };
|
|
94
|
+
if (root && source !== void 0) {
|
|
95
|
+
const draw = () => void mountLiveDriven(root, {
|
|
96
|
+
source,
|
|
97
|
+
targets: current.targets
|
|
98
|
+
});
|
|
99
|
+
draw();
|
|
100
|
+
import.meta.hot?.on(UPDATE_EVENT, draw);
|
|
101
|
+
import.meta.hot?.accept("virtual:mantaq/targets", (module) => {
|
|
102
|
+
if (module !== void 0) current.targets = module["targets"];
|
|
103
|
+
draw();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
export {};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { i as execute, r as eventCatalog } from "./run-B5huYj_r.mjs";
|
|
1
2
|
import { ANY_STATE, VIZ_SCHEMA_VERSION, coveredByAny, isAnyState, routingWarnings, unhandledSends } from "@mantaq/viz";
|
|
2
3
|
import { INITIAL_NODE_ID, buildGraph, parseInternalEventIds } from "@mantaq/traversal";
|
|
3
4
|
import { Either } from "@mantaq/utils";
|
|
4
5
|
import ts from "typescript";
|
|
5
|
-
import { dirname, join, relative, sep } from "node:path";
|
|
6
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
7
|
import { DEFAULT_SEED } from "@mantaq/pbt";
|
|
7
8
|
import { VirtualClock } from "@mantaq/core";
|
|
8
9
|
import { existsSync } from "node:fs";
|
|
9
10
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
import { runnerImport } from "vite";
|
|
11
|
+
import { loadConfigFromFile, loadEnv, runnerImport } from "vite";
|
|
11
12
|
// src/structure.ts
|
|
12
13
|
function byId$1(left, right) {
|
|
13
14
|
if (left.id < right.id) return -1;
|
|
@@ -35,7 +36,8 @@ function toState(node, context) {
|
|
|
35
36
|
final: node.isFinal,
|
|
36
37
|
effects: context.effects.find((edge) => edge.source === node.id)?.effects ?? [],
|
|
37
38
|
coverage: visits ? hits(visits.get(node.id)) : null,
|
|
38
|
-
tested: null
|
|
39
|
+
tested: null,
|
|
40
|
+
doc: null
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
43
|
function edgeKind(edge) {
|
|
@@ -91,6 +93,7 @@ function toEdge(edge, transitions) {
|
|
|
91
93
|
emits: emitsOf(edge),
|
|
92
94
|
contexts: [...new Set(edge.contexts)].sort(),
|
|
93
95
|
explanation: null,
|
|
96
|
+
doc: null,
|
|
94
97
|
coverage: transitions && kind === "transition" ? hits(edgeHits$1(edge, transitions)) : null,
|
|
95
98
|
tested: null
|
|
96
99
|
};
|
|
@@ -112,6 +115,7 @@ function toVizMachine(input) {
|
|
|
112
115
|
const events = [...new Set(edges.map((edge) => edge.event).filter(Boolean))].filter((eventId) => !eventId.startsWith("effect:")).sort().map((eventId) => ({
|
|
113
116
|
id: eventId,
|
|
114
117
|
internal: internalIds.has(eventId),
|
|
118
|
+
doc: null,
|
|
115
119
|
payloadType: null
|
|
116
120
|
}));
|
|
117
121
|
const regions = [...new Set(states.flatMap((state) => state.region ? [state.region] : []))].sort();
|
|
@@ -330,120 +334,6 @@ function createRng(seed) {
|
|
|
330
334
|
chance: (probability) => next() < probability
|
|
331
335
|
};
|
|
332
336
|
}
|
|
333
|
-
// src/fuzz/run.ts
|
|
334
|
-
const ERROR_STATE = "__error";
|
|
335
|
-
function eventTypes(options, key) {
|
|
336
|
-
if (typeof options !== "object" || options === null) return [];
|
|
337
|
-
const list = Reflect.get(options, key);
|
|
338
|
-
if (!Array.isArray(list)) return [];
|
|
339
|
-
return list.flatMap((entry) => {
|
|
340
|
-
if (typeof entry !== "object" || entry === null) return [];
|
|
341
|
-
const type = Reflect.get(entry, "type");
|
|
342
|
-
return typeof type === "string" ? [type] : [];
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
/** Declared inputs and internal events, each sorted. */
|
|
346
|
-
function eventCatalog(actor) {
|
|
347
|
-
return {
|
|
348
|
-
inputs: eventTypes(actor.options, "inputs").sort(),
|
|
349
|
-
internal: eventTypes(actor.options, "internal").sort()
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
/** onAny handlers live under this key in the dispatch map (core builder). */
|
|
353
|
-
const ANY_KEY = "Any";
|
|
354
|
-
function handles(actor, event) {
|
|
355
|
-
const transitions = actor.options?.transitions;
|
|
356
|
-
return transitions?.[actor.state.name]?.[event] !== void 0 || transitions?.[ANY_KEY]?.[event] !== void 0;
|
|
357
|
-
}
|
|
358
|
-
function toEvent(step) {
|
|
359
|
-
return step.payload === null ? { type: step.event } : {
|
|
360
|
-
type: step.event,
|
|
361
|
-
payload: step.payload
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
function messageOf(error) {
|
|
365
|
-
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
366
|
-
}
|
|
367
|
-
function featuresOf(trace) {
|
|
368
|
-
const features = /* @__PURE__ */ new Set();
|
|
369
|
-
for (const state of trace.visited) features.add(`state:${state}`);
|
|
370
|
-
for (const seen of trace.observations) features.add(seen.transitioned ? `edge:${seen.from}>${seen.event}>${seen.to}` : `stay:${seen.from}>${seen.event}`);
|
|
371
|
-
for (const error of trace.errors) features.add(`error:${error.from}>${error.event}`);
|
|
372
|
-
return [...features].sort();
|
|
373
|
-
}
|
|
374
|
-
/** JSON text of the context, or null when it is not serializable (functions, cycles). */
|
|
375
|
-
function contextText(actor) {
|
|
376
|
-
const text = Either.from(() => JSON.stringify(actor.context ?? null));
|
|
377
|
-
return Either.isRight(text) ? text[1] : null;
|
|
378
|
-
}
|
|
379
|
-
function parsed(text) {
|
|
380
|
-
return text === null ? null : JSON.parse(text);
|
|
381
|
-
}
|
|
382
|
-
function dispatch(actor, context) {
|
|
383
|
-
const { step } = context;
|
|
384
|
-
if (step.kind === "advance") context.clock.advance(step.ms);
|
|
385
|
-
else if (context.internal.has(step.event)) {
|
|
386
|
-
if (handles(actor, step.event)) actor.inject(toEvent(step));
|
|
387
|
-
} else actor.send(toEvent(step));
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Replay steps against a fresh actor. Synchronous and deterministic: effects
|
|
391
|
-
* only run via the virtual clock, and nothing here reads wall time.
|
|
392
|
-
*/
|
|
393
|
-
function execute(target, steps) {
|
|
394
|
-
const { actor, clock } = target.create();
|
|
395
|
-
const internal = new Set(eventCatalog(actor).internal);
|
|
396
|
-
const visited = [actor.state.name];
|
|
397
|
-
const observations = [];
|
|
398
|
-
const errors = [];
|
|
399
|
-
const contextSteps = [];
|
|
400
|
-
let index = 0;
|
|
401
|
-
actor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
|
|
402
|
-
const context = parsed(contextText(actor));
|
|
403
|
-
observations.push({
|
|
404
|
-
from,
|
|
405
|
-
event: event.type,
|
|
406
|
-
to,
|
|
407
|
-
transitioned,
|
|
408
|
-
effects,
|
|
409
|
-
step: index,
|
|
410
|
-
payload: event.payload,
|
|
411
|
-
context
|
|
412
|
-
});
|
|
413
|
-
if (transitioned) visited.push(to);
|
|
414
|
-
} });
|
|
415
|
-
actor.on("error", { fn: (info) => {
|
|
416
|
-
errors.push({
|
|
417
|
-
from: info.state.name,
|
|
418
|
-
event: info.event.type,
|
|
419
|
-
message: messageOf(info.error),
|
|
420
|
-
step: index
|
|
421
|
-
});
|
|
422
|
-
} });
|
|
423
|
-
for (const step of steps) {
|
|
424
|
-
if (actor.state.name === ERROR_STATE) break;
|
|
425
|
-
const before = contextText(actor);
|
|
426
|
-
dispatch(actor, {
|
|
427
|
-
step,
|
|
428
|
-
clock,
|
|
429
|
-
internal
|
|
430
|
-
});
|
|
431
|
-
if (contextText(actor) !== before) contextSteps.push(index);
|
|
432
|
-
index += 1;
|
|
433
|
-
}
|
|
434
|
-
actor.dispose();
|
|
435
|
-
return {
|
|
436
|
-
visited,
|
|
437
|
-
observations,
|
|
438
|
-
errors,
|
|
439
|
-
contextSteps,
|
|
440
|
-
features: featuresOf({
|
|
441
|
-
visited,
|
|
442
|
-
observations,
|
|
443
|
-
errors
|
|
444
|
-
})
|
|
445
|
-
};
|
|
446
|
-
}
|
|
447
337
|
// src/fuzz/fuzz.ts
|
|
448
338
|
function covers(target, steps, wanted) {
|
|
449
339
|
const reached = new Set(execute(target, steps).features);
|
|
@@ -605,6 +495,7 @@ function fuzzEdge(seen, from) {
|
|
|
605
495
|
emits: [],
|
|
606
496
|
contexts: [],
|
|
607
497
|
explanation: null,
|
|
498
|
+
doc: null,
|
|
608
499
|
coverage: null,
|
|
609
500
|
tested: null
|
|
610
501
|
};
|
|
@@ -650,6 +541,7 @@ function fuzzedMachine(target, result) {
|
|
|
650
541
|
const events = [...base.events, ...[...new Set(added.map((edge) => edge.event))].filter((id) => !known.has(id)).map((id) => ({
|
|
651
542
|
id,
|
|
652
543
|
internal: internal.has(id),
|
|
544
|
+
doc: null,
|
|
653
545
|
payloadType: null
|
|
654
546
|
}))].sort(byId);
|
|
655
547
|
return {
|
|
@@ -874,6 +766,33 @@ function machineExports(loaded) {
|
|
|
874
766
|
}).sort((left, right) => left.at - right.at).map(({ found }) => found);
|
|
875
767
|
}
|
|
876
768
|
// src/ast/program.ts
|
|
769
|
+
/**
|
|
770
|
+
* Prose of the JSDoc block found in the trivia starting at `pos`, or null when
|
|
771
|
+
* there is none. Block tags (`@param`, ...) and everything after them are cut;
|
|
772
|
+
* `{@link x}` reads as `x`.
|
|
773
|
+
*/
|
|
774
|
+
function docAt(sourceFile, pos) {
|
|
775
|
+
const block = (ts.getLeadingCommentRanges(sourceFile.text, pos) ?? []).filter((range) => sourceFile.text.startsWith("/**", range.pos)).at(-1);
|
|
776
|
+
if (block === void 0) return null;
|
|
777
|
+
const lines = sourceFile.text.slice(block.pos + 3, block.end - 2).split(/\r?\n/).map((line) => line.replace(/^\s*\* ?/, "").trim());
|
|
778
|
+
const tag = lines.findIndex((line) => line.startsWith("@"));
|
|
779
|
+
const text = (tag === -1 ? lines : lines.slice(0, tag)).join("\n").replace(/\{@link(?:code|plain)?\s+([^}|\s]+)[^}]*\}/g, "$1").trim();
|
|
780
|
+
return text === "" ? null : text;
|
|
781
|
+
}
|
|
782
|
+
function docOf(node) {
|
|
783
|
+
return docAt(node.getSourceFile(), node.getFullStart());
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Doc of one `m.on(...)` link. The first link of a chain owns the comment above
|
|
787
|
+
* the statement; later links own the comment just before their `.on`.
|
|
788
|
+
*/
|
|
789
|
+
function callDoc(call) {
|
|
790
|
+
const callee = call.expression;
|
|
791
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isCallExpression(callee.expression)) return docAt(call.getSourceFile(), callee.expression.end);
|
|
792
|
+
let top = call;
|
|
793
|
+
while (ts.isPropertyAccessExpression(top.parent) || ts.isCallExpression(top.parent)) top = top.parent;
|
|
794
|
+
return docOf(ts.isExpressionStatement(top.parent) ? top.parent : call);
|
|
795
|
+
}
|
|
877
796
|
function compilerOptions(source) {
|
|
878
797
|
const configPath = source.tsconfig ?? ts.findConfigFile(dirname(source.file), (path) => ts.sys.fileExists(path));
|
|
879
798
|
if (configPath === void 0) return {
|
|
@@ -914,7 +833,8 @@ function toRegistration(checker, call) {
|
|
|
914
833
|
event: literalProperty(checker, eventRef, "type"),
|
|
915
834
|
eventRef,
|
|
916
835
|
handler: property(options, "handler"),
|
|
917
|
-
call
|
|
836
|
+
call,
|
|
837
|
+
doc: callDoc(call)
|
|
918
838
|
};
|
|
919
839
|
}
|
|
920
840
|
function locationOf(node, root) {
|
|
@@ -926,6 +846,27 @@ function locationOf(node, root) {
|
|
|
926
846
|
column: position.character + 1
|
|
927
847
|
};
|
|
928
848
|
}
|
|
849
|
+
/** Name literal of `state("x")()` / `event("X")()`, with which builder it came from. */
|
|
850
|
+
function declaredName(init) {
|
|
851
|
+
if (!ts.isCallExpression(init) || !ts.isCallExpression(init.expression)) return null;
|
|
852
|
+
const builder = init.expression.expression;
|
|
853
|
+
const [name] = init.expression.arguments;
|
|
854
|
+
if (!ts.isIdentifier(builder) || name === void 0 || !ts.isStringLiteralLike(name)) return null;
|
|
855
|
+
return {
|
|
856
|
+
builder: builder.text,
|
|
857
|
+
name: name.text
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
function collectDeclarationDocs(statement, docs) {
|
|
861
|
+
const doc = docOf(statement);
|
|
862
|
+
if (doc === null) return;
|
|
863
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
864
|
+
const declared = declaration.initializer && declaredName(declaration.initializer);
|
|
865
|
+
if (!declared) continue;
|
|
866
|
+
if (declared.builder === "state") docs.stateDocs.set(declared.name, doc);
|
|
867
|
+
if (declared.builder === "event") docs.eventDocs.set(declared.name, doc);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
929
870
|
/**
|
|
930
871
|
* Type-check one file and collect its handler registrations. Any compiler
|
|
931
872
|
* failure becomes a Left reason; it never throws.
|
|
@@ -937,9 +878,15 @@ function loadSource(source) {
|
|
|
937
878
|
if (sourceFile === void 0) return `source not found: ${source.file}`;
|
|
938
879
|
const checker = program.getTypeChecker();
|
|
939
880
|
const registrations = [];
|
|
881
|
+
const stateDocs = /* @__PURE__ */ new Map();
|
|
882
|
+
const eventDocs = /* @__PURE__ */ new Map();
|
|
940
883
|
const visit = (node) => {
|
|
941
884
|
const found = ts.isCallExpression(node) ? toRegistration(checker, node) : null;
|
|
942
885
|
if (found) registrations.push(found);
|
|
886
|
+
if (ts.isVariableStatement(node)) collectDeclarationDocs(node, {
|
|
887
|
+
stateDocs,
|
|
888
|
+
eventDocs
|
|
889
|
+
});
|
|
943
890
|
ts.forEachChild(node, visit);
|
|
944
891
|
};
|
|
945
892
|
visit(sourceFile);
|
|
@@ -947,7 +894,9 @@ function loadSource(source) {
|
|
|
947
894
|
checker,
|
|
948
895
|
sourceFile,
|
|
949
896
|
root: source.root,
|
|
950
|
-
registrations
|
|
897
|
+
registrations,
|
|
898
|
+
stateDocs,
|
|
899
|
+
eventDocs
|
|
951
900
|
};
|
|
952
901
|
});
|
|
953
902
|
if (Either.isLeft(loaded)) return Either.left(`compiler failed: ${String(loaded[0])}`);
|
|
@@ -1477,6 +1426,7 @@ function sitesOf(loaded) {
|
|
|
1477
1426
|
return [{
|
|
1478
1427
|
state: registration.state,
|
|
1479
1428
|
event: registration.event,
|
|
1429
|
+
doc: registration.doc,
|
|
1480
1430
|
explanation: Either.getOrElse(explanation, { onLeft: (error) => ({
|
|
1481
1431
|
status: "not_analyzed",
|
|
1482
1432
|
location: null,
|
|
@@ -1490,10 +1440,17 @@ function sitesOf(loaded) {
|
|
|
1490
1440
|
function analyzeSource(source) {
|
|
1491
1441
|
return Either.chain(loadSource(source), { onRight: (loaded) => Either.right(sitesOf(loaded)) });
|
|
1492
1442
|
}
|
|
1443
|
+
function docsOf(loaded) {
|
|
1444
|
+
return {
|
|
1445
|
+
sites: sitesOf(loaded),
|
|
1446
|
+
stateDocs: loaded.stateDocs,
|
|
1447
|
+
eventDocs: loaded.eventDocs
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1493
1450
|
/** Handlers, payload samples and machine exports of a file, from one TypeScript program. */
|
|
1494
1451
|
function analyzeModule(source) {
|
|
1495
1452
|
return Either.chain(loadSource(source), { onRight: (loaded) => Either.right({
|
|
1496
|
-
|
|
1453
|
+
...docsOf(loaded),
|
|
1497
1454
|
payloads: payloadSamples(loaded),
|
|
1498
1455
|
machines: machineExports(loaded)
|
|
1499
1456
|
}) });
|
|
@@ -1503,15 +1460,24 @@ function localState(id) {
|
|
|
1503
1460
|
return id.slice(id.lastIndexOf(".") + 1);
|
|
1504
1461
|
}
|
|
1505
1462
|
function explain(edge, context) {
|
|
1506
|
-
if (edge.kind === "initial" || edge.kind === "effect") return
|
|
1463
|
+
if (edge.kind === "initial" || edge.kind === "effect") return {
|
|
1464
|
+
explanation: null,
|
|
1465
|
+
doc: null
|
|
1466
|
+
};
|
|
1507
1467
|
const state = localState(edge.from);
|
|
1508
1468
|
const site = context.sites.find((candidate) => candidate.state === state && candidate.event === edge.event) ?? context.sites.find((candidate) => candidate.state === null && candidate.event === edge.event);
|
|
1509
|
-
if (site) return
|
|
1469
|
+
if (site) return {
|
|
1470
|
+
explanation: site.explanation,
|
|
1471
|
+
doc: site.doc
|
|
1472
|
+
};
|
|
1510
1473
|
return {
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1474
|
+
explanation: {
|
|
1475
|
+
status: "not_analyzed",
|
|
1476
|
+
location: null,
|
|
1477
|
+
reason: `no m.on/m.onAny registration for (${state}, ${edge.event}) found in ${context.file}`,
|
|
1478
|
+
snippet: null
|
|
1479
|
+
},
|
|
1480
|
+
doc: null
|
|
1515
1481
|
};
|
|
1516
1482
|
}
|
|
1517
1483
|
/**
|
|
@@ -1521,7 +1487,7 @@ function explain(edge, context) {
|
|
|
1521
1487
|
*/
|
|
1522
1488
|
function annotateMachine(machine, source) {
|
|
1523
1489
|
return annotateWith(machine, {
|
|
1524
|
-
analysis:
|
|
1490
|
+
analysis: Either.chain(loadSource(source), { onRight: (loaded) => Either.right(docsOf(loaded)) }),
|
|
1525
1491
|
source
|
|
1526
1492
|
});
|
|
1527
1493
|
}
|
|
@@ -1539,19 +1505,33 @@ function annotateWith(machine, context) {
|
|
|
1539
1505
|
} : null;
|
|
1540
1506
|
return {
|
|
1541
1507
|
...edge,
|
|
1542
|
-
explanation
|
|
1508
|
+
explanation,
|
|
1509
|
+
doc: null
|
|
1543
1510
|
};
|
|
1544
1511
|
}
|
|
1545
1512
|
return {
|
|
1546
1513
|
...edge,
|
|
1547
|
-
|
|
1548
|
-
sites: analysis[1],
|
|
1514
|
+
...explain(edge, {
|
|
1515
|
+
sites: analysis[1].sites,
|
|
1549
1516
|
file
|
|
1550
1517
|
})
|
|
1551
1518
|
};
|
|
1552
1519
|
});
|
|
1520
|
+
if (Either.isLeft(analysis)) return {
|
|
1521
|
+
...machine,
|
|
1522
|
+
edges
|
|
1523
|
+
};
|
|
1524
|
+
const { stateDocs, eventDocs } = analysis[1];
|
|
1553
1525
|
return {
|
|
1554
1526
|
...machine,
|
|
1527
|
+
states: machine.states.map((state) => ({
|
|
1528
|
+
...state,
|
|
1529
|
+
doc: stateDocs.get(localState(state.id)) ?? null
|
|
1530
|
+
})),
|
|
1531
|
+
events: machine.events.map((event) => ({
|
|
1532
|
+
...event,
|
|
1533
|
+
doc: eventDocs.get(event.id) ?? null
|
|
1534
|
+
})),
|
|
1555
1535
|
edges
|
|
1556
1536
|
};
|
|
1557
1537
|
}
|
|
@@ -1583,9 +1563,8 @@ function runner(target, payloads) {
|
|
|
1583
1563
|
function annotator(target, analysis) {
|
|
1584
1564
|
const { source } = target;
|
|
1585
1565
|
if (analysis === null || source === void 0) return (machine) => machine;
|
|
1586
|
-
const sites = Either.map(analysis, { onRight: (found) => found.sites });
|
|
1587
1566
|
return (machine) => annotateWith(machine, {
|
|
1588
|
-
analysis
|
|
1567
|
+
analysis,
|
|
1589
1568
|
source
|
|
1590
1569
|
});
|
|
1591
1570
|
}
|
|
@@ -1734,6 +1713,7 @@ function vizDocument(machines) {
|
|
|
1734
1713
|
};
|
|
1735
1714
|
}
|
|
1736
1715
|
// src/load.ts
|
|
1716
|
+
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
1737
1717
|
const ENTRY = "virtual:mantaq-inspect";
|
|
1738
1718
|
const RESOLVED = `\0${ENTRY}`;
|
|
1739
1719
|
/**
|
|
@@ -1773,18 +1753,88 @@ function runOf(module) {
|
|
|
1773
1753
|
if (typeof run !== "function") return null;
|
|
1774
1754
|
return async (options) => Reflect.apply(run, void 0, [options]);
|
|
1775
1755
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1756
|
+
const ENV_ACCESS = [
|
|
1757
|
+
"import",
|
|
1758
|
+
"meta",
|
|
1759
|
+
"env"
|
|
1760
|
+
].join(".");
|
|
1761
|
+
/**
|
|
1762
|
+
* Replaces the env object with the project's env: `runnerImport` never reads
|
|
1763
|
+
* env files, so `VITE_*` variables would otherwise be missing. A textual
|
|
1764
|
+
* replace: it also rewrites matches in strings and comments, and a frozen
|
|
1765
|
+
* snapshot replaces Vite's own `import.meta.env`, so assigning to it fails.
|
|
1766
|
+
*/
|
|
1767
|
+
function projectEnv(env, mode) {
|
|
1768
|
+
const value = JSON.stringify({
|
|
1769
|
+
...env,
|
|
1770
|
+
BASE_URL: "/",
|
|
1771
|
+
MODE: mode,
|
|
1772
|
+
DEV: true,
|
|
1773
|
+
PROD: false,
|
|
1774
|
+
SSR: true
|
|
1782
1775
|
});
|
|
1776
|
+
return {
|
|
1777
|
+
name: "mantaq-inspect-env",
|
|
1778
|
+
enforce: "pre",
|
|
1779
|
+
transform: (code, id) => id.includes("/node_modules/") || !code.includes(ENV_ACCESS) ? null : {
|
|
1780
|
+
code: code.replaceAll(ENV_ACCESS, `(${value})`),
|
|
1781
|
+
map: null
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* The inline config for a load: empty without `--config`; otherwise the
|
|
1787
|
+
* project's Vite config (plugins, aliases, define) plus its env files.
|
|
1788
|
+
*/
|
|
1789
|
+
async function inlineConfig({ config, mode }) {
|
|
1790
|
+
const base = {
|
|
1791
|
+
configFile: false,
|
|
1792
|
+
logLevel: "silent"
|
|
1793
|
+
};
|
|
1794
|
+
if (config === void 0) return {
|
|
1795
|
+
...base,
|
|
1796
|
+
...mode === void 0 ? {} : { mode }
|
|
1797
|
+
};
|
|
1798
|
+
const chosen = mode ?? "development";
|
|
1799
|
+
const user = (await loadConfigFromFile({
|
|
1800
|
+
command: "serve",
|
|
1801
|
+
mode: chosen
|
|
1802
|
+
}, config))?.config ?? {};
|
|
1803
|
+
const envDir = resolve(dirname(config), user.envDir === false ? "." : user.envDir ?? ".");
|
|
1804
|
+
const env = user.envDir === false ? {} : loadEnv(chosen, envDir, user.envPrefix ?? "VITE_");
|
|
1805
|
+
return {
|
|
1806
|
+
...user,
|
|
1807
|
+
...base,
|
|
1808
|
+
mode: chosen,
|
|
1809
|
+
plugins: [...user.plugins ?? [], projectEnv(env, chosen)]
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
/** Load `file` with Vite (TS, tsconfig paths, extensionless imports) and inspect its machines. */
|
|
1813
|
+
async function inspectFile(file, options = {}, load = {}) {
|
|
1814
|
+
if (load.config !== void 0 && !existsSync(load.config)) return {
|
|
1815
|
+
machines: [],
|
|
1816
|
+
warnings: [`${file}: Vite config not found: ${load.config}`],
|
|
1817
|
+
failed: true
|
|
1818
|
+
};
|
|
1819
|
+
const loaded = await inlineConfig(load).then((inline) => runnerImport(entryId(file), {
|
|
1820
|
+
...inline,
|
|
1821
|
+
plugins: [...inline.plugins ?? [], inspectEntry()]
|
|
1822
|
+
})).then((value) => Either.right(value), (error) => Either.left((error instanceof Error ? error.message : String(error)).replace(ANSI, "")));
|
|
1823
|
+
if (Either.isLeft(loaded)) return {
|
|
1824
|
+
machines: [],
|
|
1825
|
+
warnings: [`${file}: ${loaded[0]}`],
|
|
1826
|
+
failed: true
|
|
1827
|
+
};
|
|
1828
|
+
const { module } = loaded[1];
|
|
1783
1829
|
const run = runOf(module);
|
|
1784
1830
|
if (run === null) return {
|
|
1785
1831
|
machines: [],
|
|
1786
|
-
warnings: [`${file}: could not load the inspector`]
|
|
1832
|
+
warnings: [`${file}: could not load the inspector`],
|
|
1833
|
+
failed: true
|
|
1834
|
+
};
|
|
1835
|
+
return {
|
|
1836
|
+
...await run(options),
|
|
1837
|
+
failed: false
|
|
1787
1838
|
};
|
|
1788
|
-
return run(options);
|
|
1789
1839
|
}
|
|
1790
|
-
export { toVizMachine as
|
|
1840
|
+
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 };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Either } from "@mantaq/utils";
|
|
2
|
+
// src/fuzz/run.ts
|
|
3
|
+
const ERROR_STATE = "__error";
|
|
4
|
+
function eventTypes(options, key) {
|
|
5
|
+
if (typeof options !== "object" || options === null) return [];
|
|
6
|
+
const list = Reflect.get(options, key);
|
|
7
|
+
if (!Array.isArray(list)) return [];
|
|
8
|
+
return list.flatMap((entry) => {
|
|
9
|
+
if (typeof entry !== "object" || entry === null) return [];
|
|
10
|
+
const type = Reflect.get(entry, "type");
|
|
11
|
+
return typeof type === "string" ? [type] : [];
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
/** Declared inputs and internal events, each sorted. */
|
|
15
|
+
function eventCatalog(actor) {
|
|
16
|
+
return {
|
|
17
|
+
inputs: eventTypes(actor.options, "inputs").sort(),
|
|
18
|
+
internal: eventTypes(actor.options, "internal").sort()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** onAny handlers live under this key in the dispatch map (core builder). */
|
|
22
|
+
const ANY_KEY = "Any";
|
|
23
|
+
function handles(actor, event) {
|
|
24
|
+
const transitions = actor.options?.transitions;
|
|
25
|
+
return transitions?.[actor.state.name]?.[event] !== void 0 || transitions?.[ANY_KEY]?.[event] !== void 0;
|
|
26
|
+
}
|
|
27
|
+
function toEvent(step) {
|
|
28
|
+
return step.payload === null ? { type: step.event } : {
|
|
29
|
+
type: step.event,
|
|
30
|
+
payload: step.payload
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function messageOf(error) {
|
|
34
|
+
return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
35
|
+
}
|
|
36
|
+
function featuresOf(trace) {
|
|
37
|
+
const features = /* @__PURE__ */ new Set();
|
|
38
|
+
for (const state of trace.visited) features.add(`state:${state}`);
|
|
39
|
+
for (const seen of trace.observations) features.add(seen.transitioned ? `edge:${seen.from}>${seen.event}>${seen.to}` : `stay:${seen.from}>${seen.event}`);
|
|
40
|
+
for (const error of trace.errors) features.add(`error:${error.from}>${error.event}`);
|
|
41
|
+
return [...features].sort();
|
|
42
|
+
}
|
|
43
|
+
/** JSON text of the context, or null when it is not serializable (functions, cycles). */
|
|
44
|
+
function contextText(actor) {
|
|
45
|
+
const text = Either.from(() => JSON.stringify(actor.context ?? null));
|
|
46
|
+
return Either.isRight(text) ? text[1] : null;
|
|
47
|
+
}
|
|
48
|
+
function parsed(text) {
|
|
49
|
+
return text === null ? null : JSON.parse(text);
|
|
50
|
+
}
|
|
51
|
+
/** The one step function: the fuzzer and the live driver both apply steps through it. */
|
|
52
|
+
function applyStep(actor, context) {
|
|
53
|
+
const { step } = context;
|
|
54
|
+
if (step.kind === "advance") context.clock.advance(step.ms);
|
|
55
|
+
else if (context.internal.has(step.event)) {
|
|
56
|
+
if (handles(actor, step.event)) actor.inject(toEvent(step));
|
|
57
|
+
} else actor.send(toEvent(step));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Replay steps against a fresh actor. Synchronous and deterministic: effects
|
|
61
|
+
* only run via the virtual clock, and nothing here reads wall time.
|
|
62
|
+
*/
|
|
63
|
+
function execute(target, steps) {
|
|
64
|
+
const { actor, clock } = target.create();
|
|
65
|
+
const internal = new Set(eventCatalog(actor).internal);
|
|
66
|
+
const visited = [actor.state.name];
|
|
67
|
+
const observations = [];
|
|
68
|
+
const errors = [];
|
|
69
|
+
const contextSteps = [];
|
|
70
|
+
let index = 0;
|
|
71
|
+
actor.on("transition", { fn: ({ event, from, to, transitioned, effects }) => {
|
|
72
|
+
const context = parsed(contextText(actor));
|
|
73
|
+
observations.push({
|
|
74
|
+
from,
|
|
75
|
+
event: event.type,
|
|
76
|
+
to,
|
|
77
|
+
transitioned,
|
|
78
|
+
effects,
|
|
79
|
+
step: index,
|
|
80
|
+
payload: event.payload,
|
|
81
|
+
context
|
|
82
|
+
});
|
|
83
|
+
if (transitioned) visited.push(to);
|
|
84
|
+
} });
|
|
85
|
+
actor.on("error", { fn: (info) => {
|
|
86
|
+
errors.push({
|
|
87
|
+
from: info.state.name,
|
|
88
|
+
event: info.event.type,
|
|
89
|
+
message: messageOf(info.error),
|
|
90
|
+
step: index
|
|
91
|
+
});
|
|
92
|
+
} });
|
|
93
|
+
for (const step of steps) {
|
|
94
|
+
if (actor.state.name === ERROR_STATE) break;
|
|
95
|
+
const before = contextText(actor);
|
|
96
|
+
applyStep(actor, {
|
|
97
|
+
step,
|
|
98
|
+
clock,
|
|
99
|
+
internal
|
|
100
|
+
});
|
|
101
|
+
if (contextText(actor) !== before) contextSteps.push(index);
|
|
102
|
+
index += 1;
|
|
103
|
+
}
|
|
104
|
+
actor.dispose();
|
|
105
|
+
return {
|
|
106
|
+
visited,
|
|
107
|
+
observations,
|
|
108
|
+
errors,
|
|
109
|
+
contextSteps,
|
|
110
|
+
features: featuresOf({
|
|
111
|
+
visited,
|
|
112
|
+
observations,
|
|
113
|
+
errors
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export { messageOf as a, execute as i, contextText as n, parsed as o, eventCatalog as r, applyStep as t };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { VizJson, VizStep } from "@mantaq/viz";
|
|
2
|
+
import { AnyActor, VirtualClock } from "@mantaq/core";
|
|
3
|
+
/** A machine the fuzzer can drive: a fresh actor plus the virtual clock it runs on. */
|
|
4
|
+
interface FuzzTarget {
|
|
5
|
+
id: string;
|
|
6
|
+
create(): {
|
|
7
|
+
actor: AnyActor<unknown>;
|
|
8
|
+
clock: VirtualClock;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Sample payloads per event type. Payload types are erased at runtime, so
|
|
12
|
+
* the fuzzer only mutates shapes it was given. Events without samples are
|
|
13
|
+
* sent without a payload.
|
|
14
|
+
*/
|
|
15
|
+
payloads?: Readonly<Record<string, ReadonlyArray<VizJson>>>;
|
|
16
|
+
}
|
|
17
|
+
interface Observation {
|
|
18
|
+
from: string;
|
|
19
|
+
event: string;
|
|
20
|
+
to: string;
|
|
21
|
+
transitioned: boolean;
|
|
22
|
+
/** Names of the effects that ran on entry into `to`. */
|
|
23
|
+
effects: string[];
|
|
24
|
+
/** Index of the step that caused it. */
|
|
25
|
+
step: number;
|
|
26
|
+
/** Payload the handler received (for branch-distance guidance). */
|
|
27
|
+
payload: unknown;
|
|
28
|
+
/** JSON copy of the actor context right after the handler ran; null if not JSON. */
|
|
29
|
+
context: unknown;
|
|
30
|
+
}
|
|
31
|
+
interface RunError {
|
|
32
|
+
from: string;
|
|
33
|
+
event: string;
|
|
34
|
+
message: string;
|
|
35
|
+
/** Index of the step that triggered it. */
|
|
36
|
+
step: number;
|
|
37
|
+
}
|
|
38
|
+
interface Trace {
|
|
39
|
+
visited: string[];
|
|
40
|
+
observations: Observation[];
|
|
41
|
+
errors: RunError[];
|
|
42
|
+
/** Indices of steps after which the actor context differed: loop candidates. */
|
|
43
|
+
contextSteps: number[];
|
|
44
|
+
/**
|
|
45
|
+
* Coverage features, sorted: `state:x`, `edge:a>e>b` (state changed or
|
|
46
|
+
* re-entered), `stay:a>e` (handled, no change), `error:a>e` (handler threw).
|
|
47
|
+
*/
|
|
48
|
+
features: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Declared inputs and internal events, each sorted. */
|
|
51
|
+
declare function eventCatalog(actor: AnyActor<unknown>): {
|
|
52
|
+
inputs: string[];
|
|
53
|
+
internal: string[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Replay steps against a fresh actor. Synchronous and deterministic: effects
|
|
57
|
+
* only run via the virtual clock, and nothing here reads wall time.
|
|
58
|
+
*/
|
|
59
|
+
declare function execute(target: FuzzTarget, steps: ReadonlyArray<VizStep>): Trace;
|
|
60
|
+
export { eventCatalog as a, Trace as i, Observation as n, execute as o, RunError as r, FuzzTarget as t };
|
package/dist/vite.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { c as
|
|
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-DOwsxTyL.mjs";
|
|
2
2
|
import { UPDATE_EVENT, livePageHtml, serializeViz } from "@mantaq/viz";
|
|
3
|
+
import { Either } from "@mantaq/utils";
|
|
3
4
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
5
|
import { globSync, readFileSync } from "node:fs";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -18,6 +19,42 @@ import { searchForWorkspaceRoot } from "vite";
|
|
|
18
19
|
*/
|
|
19
20
|
const STYLESHEETS = ["@mantaq/viz/viewer.css", "@mantaq/viz/page.css"];
|
|
20
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
|
+
}
|
|
21
58
|
function machineFiles(root, patterns) {
|
|
22
59
|
const files = patterns.flatMap((pattern) => globSync(pattern, { cwd: root }));
|
|
23
60
|
return [...new Set(files.map((file) => isAbsolute(file) ? file : resolve(root, file)))].sort();
|
|
@@ -40,63 +77,83 @@ function mantaqViz(options) {
|
|
|
40
77
|
const base = (options.base ?? "/__mantaq/").replace(/\/?$/, "/");
|
|
41
78
|
const patterns = Array.isArray(options.machines) ? options.machines : [options.machines];
|
|
42
79
|
const fuzz = options.fuzz ?? { iterations: DEV_ITERATIONS };
|
|
43
|
-
const live = fileURLToPath(import.meta.resolve("
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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 === TARGETS ? TARGETS_RESOLVED : null,
|
|
92
|
+
load: (id) => id === TARGETS_RESOLVED ? 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
|
+
});
|
|
65
122
|
});
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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);
|
|
78
150
|
});
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
if (path !== base && path !== base.slice(0, -1)) {
|
|
82
|
-
next();
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const html = livePageHtml({
|
|
86
|
-
title: "mantaq viz",
|
|
87
|
-
source: `${base}viz.json`,
|
|
88
|
-
entry: `/@fs${live}`,
|
|
89
|
-
css: STYLESHEETS.map((id) => readFileSync(fileURLToPath(import.meta.resolve(id)), "utf8")).join("\n")
|
|
90
151
|
});
|
|
91
|
-
server.
|
|
92
|
-
|
|
93
|
-
response.end(page);
|
|
152
|
+
server.httpServer?.once("listening", () => {
|
|
153
|
+
server.config.logger.info(` mantaq viz: ${base}`);
|
|
94
154
|
});
|
|
95
|
-
}
|
|
96
|
-
server.httpServer?.once("listening", () => {
|
|
97
|
-
server.config.logger.info(` mantaq viz: ${base}`);
|
|
98
|
-
});
|
|
155
|
+
}
|
|
99
156
|
}
|
|
100
|
-
|
|
157
|
+
];
|
|
101
158
|
}
|
|
102
159
|
export { mantaqViz };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mantaq/inspect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Turn a Mantaq machine into a viz document: structure, handler explanations (AST), fuzz and test coverage. CLI and Vite plugin included.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
"type": "module",
|
|
18
18
|
"exports": {
|
|
19
19
|
".": "./dist/index.mjs",
|
|
20
|
+
"./drive": "./dist/drive.mjs",
|
|
21
|
+
"./live-entry": "./dist/live-entry.mjs",
|
|
20
22
|
"./vite": "./dist/vite.mjs",
|
|
21
23
|
"./package.json": "./package.json"
|
|
22
24
|
},
|
|
@@ -25,11 +27,11 @@
|
|
|
25
27
|
},
|
|
26
28
|
"dependencies": {
|
|
27
29
|
"typescript": "6.0.3",
|
|
28
|
-
"@mantaq/core": "0.5.
|
|
30
|
+
"@mantaq/core": "0.5.1",
|
|
29
31
|
"@mantaq/pbt": "0.0.3",
|
|
30
|
-
"@mantaq/traversal": "0.3.
|
|
32
|
+
"@mantaq/traversal": "0.3.1",
|
|
31
33
|
"@mantaq/utils": "0.0.4",
|
|
32
|
-
"@mantaq/viz": "0.
|
|
34
|
+
"@mantaq/viz": "0.2.0"
|
|
33
35
|
},
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"@types/node": "^26.2.0",
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
"happy-dom": "^21.0.4",
|
|
38
40
|
"vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
|
|
39
41
|
"vite-plus": "1.4.0",
|
|
40
|
-
"@mantaq/sugar": "2.0.
|
|
42
|
+
"@mantaq/sugar": "2.0.1"
|
|
41
43
|
},
|
|
42
44
|
"peerDependencies": {
|
|
43
45
|
"vite": "*"
|