waygraph 0.16.1 → 0.16.4
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 +12 -0
- package/dist/analysis/practices-check.d.ts +1 -1
- package/dist/analysis/practices-check.js +36 -0
- package/dist/beta.d.ts +6 -0
- package/dist/beta.js +5 -0
- package/dist/cli/commands/codemod-flows.js +2 -2
- package/dist/cli/commands/codemod-stubs.js +132 -2
- package/dist/cli/commands/pilot.js +3 -16
- package/dist/cli/commands/session-ops.js +26 -1
- package/dist/cli/flags.d.ts +1 -1
- package/dist/cli/flags.js +1 -1
- package/dist/cli/run-chain.js +2 -2
- package/dist/cli/usage.js +6 -0
- package/dist/cli/util.js +1 -1
- package/dist/demo/narration/demo-pace.d.ts +1 -1
- package/dist/demo/narration/demo-pace.js +1 -1
- package/dist/demo/narration/merge.d.ts +1 -1
- package/dist/demo/narration/merge.js +1 -1
- package/dist/demo/narration/narration.d.ts +2 -6
- package/dist/demo/narration/narration.js +5 -17
- package/dist/demo/narration/stub-ctx.js +126 -124
- package/dist/demo/overlay/cursor.js +1 -1
- package/dist/demo/overlay/inpage/banner.js +5 -0
- package/dist/demo/overlay/inpage/core.js +1 -6
- package/dist/demo/overlay/inpage/network.js +1 -1
- package/dist/demo/overlay/inpage/todos.js +0 -15
- package/dist/demo/overlay/network-capture.js +2 -2
- package/dist/demo/overlay/overlay-css.js +19 -76
- package/dist/demo/overlay/overlay-stage.js +1 -1
- package/dist/demo/overlay/overlay.js +3 -2
- package/dist/demo/overlay/phase-fixtures.js +5 -5
- package/dist/demo/overlay/pilot/shell.js +7 -5
- package/dist/demo/overlay/rings.d.ts +0 -8
- package/dist/demo/overlay/rings.js +0 -15
- package/dist/demo/overlay/ui/components/ring.js +2 -2
- package/dist/demo/overlay/ui/overlay.css +136 -33
- package/dist/demo/overlay/ui/shadow.js +69 -2
- package/dist/demo/stepper/after.js +4 -17
- package/dist/demo/stepper/before.js +3 -1
- package/dist/demo/stepper/demo-log.js +8 -8
- package/dist/demo/stepper/interaction-patch.js +3 -15
- package/dist/demo/stepper/panel-after.js +1 -1
- package/dist/demo/stepper/slides.d.ts +3 -0
- package/dist/demo/stepper/slides.js +18 -2
- package/dist/demo/stepper/stepper.js +1 -1
- package/dist/demo/stepper/teardown.js +0 -1
- package/dist/demo/verbose-log.js +1 -1
- package/dist/engine/block/block.d.ts +1 -12
- package/dist/engine/block/kind.d.ts +3 -0
- package/dist/engine/block/kind.js +11 -0
- package/dist/engine/block/kinds/method.d.ts +0 -2
- package/dist/engine/block/kinds/method.js +0 -1
- package/dist/engine/block/kinds/nav.js +1 -1
- package/dist/engine/errors.d.ts +12 -0
- package/dist/engine/errors.js +12 -0
- package/dist/engine/read/read-block.d.ts +61 -0
- package/dist/engine/read/read-block.js +94 -0
- package/dist/engine/read/read-key.d.ts +32 -0
- package/dist/engine/read/read-key.js +34 -0
- package/dist/engine/read/read-page.d.ts +19 -0
- package/dist/engine/read/read-page.js +50 -0
- package/dist/engine/read/readers.d.ts +82 -0
- package/dist/engine/read/readers.js +43 -0
- package/dist/engine/read/run-read.d.ts +17 -0
- package/dist/engine/read/run-read.js +156 -0
- package/dist/engine/run/connect.js +3 -2
- package/dist/engine/run/events.d.ts +28 -10
- package/dist/engine/run/events.js +52 -38
- package/dist/engine/run/run.d.ts +1 -19
- package/dist/engine/run/run.js +21 -97
- package/dist/engine/stub/ring.d.ts +2 -4
- package/dist/engine/stub/ring.js +1 -0
- package/dist/engine/stub/stub.d.ts +38 -25
- package/dist/index.d.ts +9 -7
- package/dist/index.js +11 -6
- package/dist/live/auto/auto.js +2 -3
- package/dist/live/auto/menu.js +3 -2
- package/dist/live/auto-session/auto-session.d.ts +9 -2
- package/dist/live/auto-session/auto-session.js +23 -20
- package/dist/live/auto-session/feed.d.ts +16 -0
- package/dist/live/auto-session/feed.js +71 -0
- package/dist/live/auto-session/helpers.d.ts +7 -0
- package/dist/live/auto-session/helpers.js +9 -0
- package/dist/live/auto-session/ipc.d.ts +15 -47
- package/dist/live/auto-session/ipc.js +34 -4
- package/dist/live/auto-session/protocol.d.ts +127 -0
- package/dist/live/auto-session/protocol.js +6 -0
- package/dist/live/auto-session/read.d.ts +25 -0
- package/dist/live/auto-session/read.js +50 -0
- package/dist/live/traverse/worker.d.ts +1 -0
- package/dist/live/traverse/worker.js +6 -1
- package/dist/manifest/manifest.d.ts +22 -2
- package/dist/manifest/manifest.js +27 -1
- package/dist/manifest/project.d.ts +10 -1
- package/dist/manifest/project.js +9 -12
- package/dist/manifest/read-blocks.d.ts +2 -0
- package/dist/manifest/read-blocks.js +23 -0
- package/dist/manifest/scan.d.ts +3 -2
- package/dist/manifest/scan.js +8 -4
- package/dist/runner/plan/find.js +2 -2
- package/dist/runner/plan/plan.js +1 -1
- package/dist/runner/plan/seed-mem.js +5 -5
- package/dist/runner/plan/spec.js +1 -1
- package/dist/runner/runner.d.ts +0 -1
- package/dist/runner/runner.js +6 -6
- package/dist/runtime/full-launch.d.ts +2 -1
- package/dist/runtime/full-launch.js +29 -27
- package/dist/runtime/run-log.d.ts +2 -0
- package/dist/runtime/run-log.js +13 -6
- package/examples/saucedemo/README.md +2 -2
- package/examples/saucedemo/src/blocks/SITE-MAP.md +3 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/cart/cart.read.block.ts +26 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/examples/saucedemo/src/blocks/saucedemo-web/header.read.block.ts +25 -0
- package/examples/saucedemo/src/blocks/saucedemo-web/inventory/inventory.read.block.ts +28 -0
- package/examples/saucedemo/src/flows/api-demo.flow.ts +1 -1
- package/examples/saucedemo/src/flows/cart-bulk.flow.ts +1 -1
- package/examples/saucedemo/src/flows/cart-continue.flow.ts +1 -1
- package/examples/saucedemo/src/flows/catalog-logout.flow.ts +1 -1
- package/examples/saucedemo/src/flows/checkout-then-browse-again.flow.ts +3 -2
- package/examples/saucedemo/src/flows/checkout.flow.ts +13 -12
- package/examples/saucedemo/src/flows/login.flow.ts +1 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +3 -2
- package/examples/saucedemo/src/flows/stub-on-error.flow.ts +1 -1
- package/examples/saucedemo/src/flows/viewer-blocked.flow.ts +1 -1
- package/examples/saucedemo/src/states/checkout.reads.ts +9 -0
- package/examples/saucedemo/src/stubs/device-showcase.ts +70 -76
- package/examples/saucedemo/src/stubs/finish-order.ts +38 -0
- package/examples/saucedemo/src/stubs/login.ts +5 -5
- package/package.json +5 -1
- package/templates/quickstart/src/blocks/saucedemo-web/checkout-step-two/methods/finish-order.method.block.ts +2 -34
- package/templates/quickstart/src/flows/api-demo.flow.ts +1 -1
- package/templates/quickstart/src/flows/cart-bulk.flow.ts +1 -1
- package/templates/quickstart/src/flows/cart-continue.flow.ts +1 -1
- package/templates/quickstart/src/flows/catalog-logout.flow.ts +1 -1
- package/templates/quickstart/src/flows/checkout-then-browse-again.flow.ts +3 -2
- package/templates/quickstart/src/flows/checkout.flow.ts +13 -12
- package/templates/quickstart/src/flows/login.flow.ts +1 -1
- package/templates/quickstart/src/flows/shop.flow.ts +3 -2
- package/templates/quickstart/src/flows/stub-on-error.flow.ts +1 -1
- package/templates/quickstart/src/flows/viewer-blocked.flow.ts +1 -1
- package/templates/quickstart/src/stubs/device-showcase.ts +70 -76
- package/templates/quickstart/src/stubs/finish-order.ts +38 -0
- package/templates/quickstart/src/stubs/login.ts +5 -5
- package/templates/scaffold/src/flows/(external)/hoppscotch/auth.flow.ts +1 -1
- package/templates/scaffold/src/flows/(external)/hoppscotch/get.flow.ts +1 -1
- package/templates/scaffold/src/flows/(external)/hoppscotch/post.flow.ts +1 -1
- package/templates/scaffold/src/flows/(user-stories)/email-verification.flow.ts +4 -4
- package/templates/scaffold/src/flows/README.md +1 -1
- package/templates/scaffold/src/flows/home/cart.flow.ts +7 -7
- package/templates/scaffold/src/flows/home/hello.flow.ts +10 -7
- package/templates/scaffold/src/map/(app_base)/home/_methods/assert-hello.method.block.ts +1 -9
- package/templates/skills/waygraph-convention.skill.md +10 -2
- package/templates/skills/waygraph-pilot.skill.md +7 -0
|
@@ -1,52 +1,8 @@
|
|
|
1
1
|
import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
|
|
2
2
|
import { AutoSession } from "./auto-session.js";
|
|
3
3
|
import { SessionMeta } from "./files.js";
|
|
4
|
-
import { type
|
|
5
|
-
|
|
6
|
-
ok: true;
|
|
7
|
-
snapshot: SessionSnapshot;
|
|
8
|
-
quit: boolean;
|
|
9
|
-
} | {
|
|
10
|
-
ok: false;
|
|
11
|
-
error: string;
|
|
12
|
-
};
|
|
13
|
-
export type DomResponse = {
|
|
14
|
-
ok: true;
|
|
15
|
-
snapshot: DomSnapshot;
|
|
16
|
-
} | {
|
|
17
|
-
ok: false;
|
|
18
|
-
error: string;
|
|
19
|
-
};
|
|
20
|
-
export type TraceResponse = {
|
|
21
|
-
ok: true;
|
|
22
|
-
trace: TraceStep[];
|
|
23
|
-
} | {
|
|
24
|
-
ok: false;
|
|
25
|
-
error: string;
|
|
26
|
-
};
|
|
27
|
-
export type ConsoleResponse = {
|
|
28
|
-
ok: true;
|
|
29
|
-
consoleLog: ConsoleLogEntry[];
|
|
30
|
-
} | {
|
|
31
|
-
ok: false;
|
|
32
|
-
error: string;
|
|
33
|
-
};
|
|
34
|
-
export type StorageResponse = ({
|
|
35
|
-
ok: true;
|
|
36
|
-
} & StorageSnapshot) | {
|
|
37
|
-
ok: false;
|
|
38
|
-
error: string;
|
|
39
|
-
};
|
|
40
|
-
export type ReachResponse = ApplyPathResult;
|
|
41
|
-
export type HighlightResponse = {
|
|
42
|
-
ok: true;
|
|
43
|
-
painted: number;
|
|
44
|
-
missing: string[];
|
|
45
|
-
snapshot: SessionSnapshot;
|
|
46
|
-
} | {
|
|
47
|
-
ok: false;
|
|
48
|
-
error: string;
|
|
49
|
-
};
|
|
4
|
+
import { type AutoSessionInit, type InspectDomOptions, type StubFileKind } from "./helpers.js";
|
|
5
|
+
import type { HelloResponse, ConsoleResponse, DomResponse, HighlightResponse, ReachResponse, ReadResponse, StatusOrSendResponse, StorageResponse, TraceResponse } from "./protocol.js";
|
|
50
6
|
/**
|
|
51
7
|
* Client-side: sends one request to a running session and returns its
|
|
52
8
|
* response. Reports a stale/unreachable session as an error within
|
|
@@ -94,6 +50,16 @@ export declare function requestSession(projectDir: string, sessionId: string, re
|
|
|
94
50
|
export declare function requestSession(projectDir: string, sessionId: string, request: {
|
|
95
51
|
op: "storage";
|
|
96
52
|
}, timeoutMs?: number): Promise<StorageResponse>;
|
|
53
|
+
export declare function requestSession(projectDir: string, sessionId: string, request: {
|
|
54
|
+
op: "hello";
|
|
55
|
+
}, timeoutMs?: number): Promise<HelloResponse | {
|
|
56
|
+
ok: false;
|
|
57
|
+
error: string;
|
|
58
|
+
}>;
|
|
59
|
+
export declare function requestSession(projectDir: string, sessionId: string, request: {
|
|
60
|
+
op: "read";
|
|
61
|
+
path?: string;
|
|
62
|
+
}, timeoutMs?: number): Promise<ReadResponse>;
|
|
97
63
|
export declare function requestSession(projectDir: string, sessionId: string, request: {
|
|
98
64
|
op: "reach";
|
|
99
65
|
checkpoint: string;
|
|
@@ -110,7 +76,9 @@ export declare function requestSession(projectDir: string, sessionId: string, re
|
|
|
110
76
|
* already prevents true parallelism, but two overlapping `await`-heavy
|
|
111
77
|
* handlers could still interleave against the same page/mem.
|
|
112
78
|
*/
|
|
113
|
-
export declare function serveSession(session: AutoSession, projectDir: string, sessionId: string, headless?: boolean
|
|
79
|
+
export declare function serveSession(session: AutoSession, projectDir: string, sessionId: string, headless?: boolean,
|
|
80
|
+
/** After a quit closed the session and removed its files: the session process exits here. */
|
|
81
|
+
onShutdown?: () => void): Promise<void>;
|
|
114
82
|
/** Entry point for the hidden `__auto-serve` CLI command (runs in the detached child). */
|
|
115
83
|
export declare function runAutoServeCommand(init: AutoSessionInit, sessionId: string): Promise<void>;
|
|
116
84
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* See openspec/changes/waygraph-auto-cli-session-control.
|
|
6
6
|
*/
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
|
-
import { mkdirSync, openSync, rmSync, writeFileSync, } from "node:fs";
|
|
8
|
+
import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import { connect, createServer } from "node:net";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
@@ -15,6 +15,8 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { ignorePageGone } from "../../engine/page-gone.js";
|
|
16
16
|
import { AutoSession } from "./auto-session.js";
|
|
17
17
|
import { generateSessionId, isPidAlive, logPathFor, metaPath, readSessionMeta, removeSessionFiles, resolveSessionMeta, sessionDir, socketDir, socketPathFor, writeGlobalSessionMeta } from "./files.js";
|
|
18
|
+
import { createFeed } from "./feed.js";
|
|
19
|
+
import { SESSION_PROTOCOL_VERSION } from "./protocol.js";
|
|
18
20
|
/** Reads newline-delimited JSON off a socket, one message at a time. */
|
|
19
21
|
function readOneMessage(sock, timeoutMs) {
|
|
20
22
|
return new Promise((resolvePromise, reject) => {
|
|
@@ -109,7 +111,9 @@ export async function requestSession(projectDir, sessionId, request, timeoutMs =
|
|
|
109
111
|
* already prevents true parallelism, but two overlapping `await`-heavy
|
|
110
112
|
* handlers could still interleave against the same page/mem.
|
|
111
113
|
*/
|
|
112
|
-
export async function serveSession(session, projectDir, sessionId, headless = true
|
|
114
|
+
export async function serveSession(session, projectDir, sessionId, headless = true,
|
|
115
|
+
/** After a quit closed the session and removed its files: the session process exits here. */
|
|
116
|
+
onShutdown = () => { }) {
|
|
113
117
|
mkdirSync(sessionDir(projectDir), { recursive: true });
|
|
114
118
|
mkdirSync(socketDir(), { recursive: true });
|
|
115
119
|
const socketPath = socketPathFor(sessionId);
|
|
@@ -120,6 +124,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
120
124
|
queue = next.catch(() => { });
|
|
121
125
|
return next;
|
|
122
126
|
};
|
|
127
|
+
const feed = createFeed();
|
|
123
128
|
const server = createServer((sock) => {
|
|
124
129
|
let buf = "";
|
|
125
130
|
sock.on("data", (chunk) => {
|
|
@@ -141,6 +146,15 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
141
146
|
sock.write(JSON.stringify({ ok: false, error: "malformed request (invalid JSON)" }) + "\n");
|
|
142
147
|
return;
|
|
143
148
|
}
|
|
149
|
+
// Neither waits behind a running request: hello names the protocol, subscribe turns this socket into a live feed.
|
|
150
|
+
if (request.op === "hello") {
|
|
151
|
+
sock.write(JSON.stringify({ ok: true, protocol: SESSION_PROTOCOL_VERSION, waygraph: waygraphVersion(), sessionId, here: session.where }) + "\n");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (request.op === "subscribe") {
|
|
155
|
+
feed.add(sock);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
144
158
|
await enqueue(async () => {
|
|
145
159
|
let response;
|
|
146
160
|
let shouldQuit = false;
|
|
@@ -165,6 +179,9 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
165
179
|
else if (request.op === "console") {
|
|
166
180
|
response = { ok: true, consoleLog: session.getConsoleLog() };
|
|
167
181
|
}
|
|
182
|
+
else if (request.op === "read") {
|
|
183
|
+
response = { ok: true, ...(await session.read(request.path)) };
|
|
184
|
+
}
|
|
168
185
|
else if (request.op === "storage") {
|
|
169
186
|
response = { ok: true, ...(await session.inspectStorage()) };
|
|
170
187
|
}
|
|
@@ -213,6 +230,9 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
213
230
|
response = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
214
231
|
}
|
|
215
232
|
sock.write(JSON.stringify(response) + "\n");
|
|
233
|
+
// Whatever changed (a step ran, a reload, a raw action): subscribers get the new snapshot.
|
|
234
|
+
if (response.ok && "snapshot" in response && "sections" in response.snapshot)
|
|
235
|
+
feed.send({ type: "snapshot", snapshot: response.snapshot });
|
|
216
236
|
if (shouldQuit) {
|
|
217
237
|
setImmediate(() => void shutdown());
|
|
218
238
|
}
|
|
@@ -224,9 +244,10 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
224
244
|
return;
|
|
225
245
|
shuttingDown = true;
|
|
226
246
|
server.close();
|
|
247
|
+
feed.close();
|
|
227
248
|
await session.close().catch(ignorePageGone);
|
|
228
249
|
removeSessionFiles(projectDir, sessionId);
|
|
229
|
-
|
|
250
|
+
onShutdown();
|
|
230
251
|
}
|
|
231
252
|
await new Promise((resolvePromise, reject) => {
|
|
232
253
|
server.on("error", reject);
|
|
@@ -248,7 +269,7 @@ export async function serveSession(session, projectDir, sessionId, headless = tr
|
|
|
248
269
|
/** Entry point for the hidden `__auto-serve` CLI command (runs in the detached child). */
|
|
249
270
|
export async function runAutoServeCommand(init, sessionId) {
|
|
250
271
|
const session = await AutoSession.start({ ...init, sessionId });
|
|
251
|
-
await serveSession(session, init.projectDir, sessionId, init.headless ?? true);
|
|
272
|
+
await serveSession(session, init.projectDir, sessionId, init.headless ?? true, () => process.exit(0));
|
|
252
273
|
// Tell the parent that spawned us (spawnDetachedSession) we are serving, then let it go.
|
|
253
274
|
process.send?.({ ready: true });
|
|
254
275
|
process.disconnect?.();
|
|
@@ -372,3 +393,12 @@ export async function runAttachLoop(projectDir, sessionId) {
|
|
|
372
393
|
rl.close();
|
|
373
394
|
}
|
|
374
395
|
}
|
|
396
|
+
/** This package's version, for `hello` (the file sits three levels below the package root in src/ and dist/). */
|
|
397
|
+
function waygraphVersion() {
|
|
398
|
+
try {
|
|
399
|
+
return JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "package.json"), "utf8")).version;
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
return "unknown";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { PilotHighlightFixtures } from "../../demo/overlay/pilot/fixtures.js";
|
|
2
|
+
import type { RunEvent } from "../../engine/run/events.js";
|
|
3
|
+
import type { ApplyPathResult, ConsoleLogEntry, DomSnapshot, InspectDomOptions, SessionSnapshot, StorageSnapshot, StubFileKind, TraceStep } from "./helpers.js";
|
|
4
|
+
import type { SessionRead } from "./read.js";
|
|
5
|
+
/** Bumped when a request or response changes shape (adding an op or an optional field does not). */
|
|
6
|
+
export declare const SESSION_PROTOCOL_VERSION = 1;
|
|
7
|
+
/** Every request a session answers. */
|
|
8
|
+
export type SessionRequest = {
|
|
9
|
+
op: "hello";
|
|
10
|
+
} | {
|
|
11
|
+
op: "subscribe";
|
|
12
|
+
} | {
|
|
13
|
+
op: "status";
|
|
14
|
+
} | {
|
|
15
|
+
op: "send";
|
|
16
|
+
pick: string;
|
|
17
|
+
} | ({
|
|
18
|
+
op: "dom";
|
|
19
|
+
} & InspectDomOptions) | {
|
|
20
|
+
op: "trace";
|
|
21
|
+
} | {
|
|
22
|
+
op: "click";
|
|
23
|
+
selector: string;
|
|
24
|
+
} | {
|
|
25
|
+
op: "type";
|
|
26
|
+
selector: string;
|
|
27
|
+
text: string;
|
|
28
|
+
} | {
|
|
29
|
+
op: "press";
|
|
30
|
+
selector: string;
|
|
31
|
+
key: string;
|
|
32
|
+
} | {
|
|
33
|
+
op: "goto";
|
|
34
|
+
url: string;
|
|
35
|
+
} | {
|
|
36
|
+
op: "reload";
|
|
37
|
+
} | {
|
|
38
|
+
op: "reach";
|
|
39
|
+
checkpoint: string;
|
|
40
|
+
} | {
|
|
41
|
+
op: "resync";
|
|
42
|
+
} | {
|
|
43
|
+
op: "console";
|
|
44
|
+
} | {
|
|
45
|
+
op: "storage";
|
|
46
|
+
} | {
|
|
47
|
+
op: "read";
|
|
48
|
+
path?: string;
|
|
49
|
+
} | {
|
|
50
|
+
op: "upload";
|
|
51
|
+
selector: string;
|
|
52
|
+
stub: StubFileKind | {
|
|
53
|
+
filePath: string;
|
|
54
|
+
};
|
|
55
|
+
} | ({
|
|
56
|
+
op: "highlight";
|
|
57
|
+
} & PilotHighlightFixtures);
|
|
58
|
+
export type StatusOrSendResponse = {
|
|
59
|
+
ok: true;
|
|
60
|
+
snapshot: SessionSnapshot;
|
|
61
|
+
quit: boolean;
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
error: string;
|
|
65
|
+
};
|
|
66
|
+
export type DomResponse = {
|
|
67
|
+
ok: true;
|
|
68
|
+
snapshot: DomSnapshot;
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
error: string;
|
|
72
|
+
};
|
|
73
|
+
export type TraceResponse = {
|
|
74
|
+
ok: true;
|
|
75
|
+
trace: TraceStep[];
|
|
76
|
+
} | {
|
|
77
|
+
ok: false;
|
|
78
|
+
error: string;
|
|
79
|
+
};
|
|
80
|
+
export type ConsoleResponse = {
|
|
81
|
+
ok: true;
|
|
82
|
+
consoleLog: ConsoleLogEntry[];
|
|
83
|
+
} | {
|
|
84
|
+
ok: false;
|
|
85
|
+
error: string;
|
|
86
|
+
};
|
|
87
|
+
export type StorageResponse = ({
|
|
88
|
+
ok: true;
|
|
89
|
+
} & StorageSnapshot) | {
|
|
90
|
+
ok: false;
|
|
91
|
+
error: string;
|
|
92
|
+
};
|
|
93
|
+
export type ReachResponse = ApplyPathResult;
|
|
94
|
+
export type ReadResponse = ({
|
|
95
|
+
ok: true;
|
|
96
|
+
} & SessionRead) | {
|
|
97
|
+
ok: false;
|
|
98
|
+
error: string;
|
|
99
|
+
};
|
|
100
|
+
export type HighlightResponse = {
|
|
101
|
+
ok: true;
|
|
102
|
+
painted: number;
|
|
103
|
+
missing: string[];
|
|
104
|
+
snapshot: SessionSnapshot;
|
|
105
|
+
} | {
|
|
106
|
+
ok: false;
|
|
107
|
+
error: string;
|
|
108
|
+
};
|
|
109
|
+
/** What `hello` answers: the protocol a client is talking to, and where the session is (`null` until a status or step has located it). */
|
|
110
|
+
export type HelloResponse = {
|
|
111
|
+
ok: true;
|
|
112
|
+
protocol: number;
|
|
113
|
+
waygraph: string;
|
|
114
|
+
sessionId: string;
|
|
115
|
+
here: string | null;
|
|
116
|
+
};
|
|
117
|
+
/** One line of a `subscribe` feed: every run event in the session (steps, reads), and the snapshot after each change. */
|
|
118
|
+
export type SessionStreamMessage = {
|
|
119
|
+
type: "event";
|
|
120
|
+
event: RunEvent;
|
|
121
|
+
} | {
|
|
122
|
+
type: "snapshot";
|
|
123
|
+
snapshot: SessionSnapshot;
|
|
124
|
+
};
|
|
125
|
+
export type SessionResponse = HelloResponse | StatusOrSendResponse | DomResponse | TraceResponse | ConsoleResponse | StorageResponse | ReachResponse | ReadResponse | HighlightResponse;
|
|
126
|
+
/** Every op, once: the server, the CLI and `pilot` all check against this list. */
|
|
127
|
+
export declare const SESSION_OPS: readonly ["hello", "subscribe", "status", "send", "dom", "trace", "console", "storage", "read", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Bumped when a request or response changes shape (adding an op or an optional field does not). */
|
|
2
|
+
export const SESSION_PROTOCOL_VERSION = 1;
|
|
3
|
+
/** Every op, once: the server, the CLI and `pilot` all check against this list. */
|
|
4
|
+
export const SESSION_OPS = ["hello", "subscribe", "status", "send", "dom", "trace", "console", "storage", "read", "upload", "click", "type", "press", "goto", "reload", "reach", "resync", "highlight"];
|
|
5
|
+
const complete = true;
|
|
6
|
+
void complete;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Page } from "@playwright/test";
|
|
2
|
+
import type { MemPage } from "../../engine/mem/mem.js";
|
|
3
|
+
/** The answer to `waygraph pilot read <id> [path]`. */
|
|
4
|
+
export interface SessionRead {
|
|
5
|
+
page: string | null;
|
|
6
|
+
url: string;
|
|
7
|
+
read: Record<string, unknown>;
|
|
8
|
+
missing: string[];
|
|
9
|
+
notes: Record<string, string>;
|
|
10
|
+
readBlocks: string[];
|
|
11
|
+
skipped: {
|
|
12
|
+
block: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
}[];
|
|
15
|
+
/** Set when no Read Block covers this page. */
|
|
16
|
+
hint?: string;
|
|
17
|
+
}
|
|
18
|
+
/** Reads the page the session is on (`here`); with `path`, only that part (`post.comments`). */
|
|
19
|
+
export declare function readSession(page: Page, mem: MemPage, here: string | null, path?: string): Promise<SessionRead>;
|
|
20
|
+
/** The read a status / send / reach snapshot carries: lists cut to a few items, nothing at all without Read Blocks. */
|
|
21
|
+
export declare function snapshotRead(page: Page, mem: MemPage, here: string | null): Promise<{
|
|
22
|
+
read?: Record<string, unknown>;
|
|
23
|
+
missing?: string[];
|
|
24
|
+
readNotes?: Record<string, string>;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readBlocksFor, readCheckpoint } from "../../engine/read/read-block.js";
|
|
2
|
+
/** A snapshot shows at most this many items of a list; `read <path>` returns the rest. */
|
|
3
|
+
const SNAPSHOT_LIST_CAP = 20;
|
|
4
|
+
function pick(tree, path) {
|
|
5
|
+
const parts = path.split(".");
|
|
6
|
+
let node = tree;
|
|
7
|
+
for (const part of parts)
|
|
8
|
+
node = node !== null && typeof node === "object" ? node[part] : undefined;
|
|
9
|
+
const out = {};
|
|
10
|
+
let at = out;
|
|
11
|
+
for (const part of parts.slice(0, -1))
|
|
12
|
+
at = at[part] = {};
|
|
13
|
+
at[parts[parts.length - 1]] = node ?? null;
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
/** Reads the page the session is on (`here`); with `path`, only that part (`post.comments`). */
|
|
17
|
+
export async function readSession(page, mem, here, path) {
|
|
18
|
+
const base = { page: here, url: page.url() };
|
|
19
|
+
if (readBlocksFor(here).length === 0) {
|
|
20
|
+
return { ...base, read: {}, missing: [], notes: {}, readBlocks: [], skipped: [], hint: "no Read Block for this page: use `dom` to look at it" };
|
|
21
|
+
}
|
|
22
|
+
const { read, blocks, skipped } = await readCheckpoint(page, mem, here);
|
|
23
|
+
const inPath = (p) => !path || p === path || p.startsWith(path + ".");
|
|
24
|
+
return {
|
|
25
|
+
...base,
|
|
26
|
+
read: path ? pick(read.toJSON(), path) : read.toJSON(),
|
|
27
|
+
missing: read.missing.filter(inPath),
|
|
28
|
+
notes: Object.fromEntries(Object.entries(read.notes).filter(([p]) => inPath(p))),
|
|
29
|
+
readBlocks: blocks,
|
|
30
|
+
skipped,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** The read a status / send / reach snapshot carries: lists cut to a few items, nothing at all without Read Blocks. */
|
|
34
|
+
export async function snapshotRead(page, mem, here) {
|
|
35
|
+
if (readBlocksFor(here).length === 0)
|
|
36
|
+
return {};
|
|
37
|
+
const got = await readSession(page, mem, here);
|
|
38
|
+
const notes = { ...got.notes };
|
|
39
|
+
const cap = (node, path) => {
|
|
40
|
+
if (Array.isArray(node)) {
|
|
41
|
+
if (node.length > SNAPSHOT_LIST_CAP)
|
|
42
|
+
notes[path] = `${SNAPSHOT_LIST_CAP} of ${node.length} shown: \`read <id> ${path}\` for all`;
|
|
43
|
+
return node.slice(0, SNAPSHOT_LIST_CAP);
|
|
44
|
+
}
|
|
45
|
+
if (node === null || typeof node !== "object")
|
|
46
|
+
return node;
|
|
47
|
+
return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, cap(v, path ? `${path}.${k}` : k)]));
|
|
48
|
+
};
|
|
49
|
+
return { read: cap(got.read, ""), missing: got.missing, ...(Object.keys(notes).length ? { readNotes: notes } : {}) };
|
|
50
|
+
}
|
|
@@ -34,4 +34,5 @@ export interface WorkerOpts {
|
|
|
34
34
|
mem: MemPage;
|
|
35
35
|
leases: EdgeLeaseCoordinator;
|
|
36
36
|
}
|
|
37
|
+
/** One worker's walk, as one run of its own (parallel workers never share a run id or step counter). */
|
|
37
38
|
export declare function runTraverseWorker(opts: WorkerOpts): Promise<TraverseWorkerResult>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { envString } from "../../engine/env.js";
|
|
2
2
|
import { buildFlow } from "../../engine/flow/flow.js";
|
|
3
3
|
import { ignorePageGone } from "../../engine/page-gone.js";
|
|
4
|
+
import { inRun } from "../../engine/run/events.js";
|
|
4
5
|
import { detectHere } from "../auto/auto.js";
|
|
5
6
|
import { buildExploreMenu } from "../auto/explore.js";
|
|
6
7
|
import { memStubValue } from "../auto/mem.js";
|
|
@@ -83,7 +84,11 @@ export async function runOneBlock(engine, entry, context, page, mem) {
|
|
|
83
84
|
clearTimeout(timer);
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
/** One worker's walk, as one run of its own (parallel workers never share a run id or step counter). */
|
|
88
|
+
export function runTraverseWorker(opts) {
|
|
89
|
+
return inRun(`traverse ${opts.traverseId} worker ${opts.workerIndex}`, () => walk(opts), (r) => r.leaf);
|
|
90
|
+
}
|
|
91
|
+
async function walk(opts) {
|
|
87
92
|
const { traverseId, workerIndex, parallel, maxSteps, maxVisitsPerNode, maxVisitsPerEdge, timeoutMs, graph, library, engine, context, mem, leases, } = opts;
|
|
88
93
|
let page = opts.page;
|
|
89
94
|
const startUrl = opts.startUrl;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type { BlockKind
|
|
2
|
-
|
|
1
|
+
import type { BlockKind } from "../engine/block/kind.js";
|
|
2
|
+
import type { ProjectScan, ScanOptions } from "./project.js";
|
|
3
|
+
import { type ReadShape } from "../engine/read/read-block.js";
|
|
4
|
+
export declare const MANIFEST_VERSION = 2;
|
|
3
5
|
export interface ManifestBlock {
|
|
4
6
|
/** `file#exportName`, unique across the project. */
|
|
5
7
|
id: string;
|
|
@@ -33,6 +35,22 @@ export interface ManifestFlow {
|
|
|
33
35
|
demoPace?: unknown;
|
|
34
36
|
};
|
|
35
37
|
}
|
|
38
|
+
/** A Read Block (beta): which Checkpoints it reads and the shape of what it returns. */
|
|
39
|
+
export interface ManifestReadBlock {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
exportName: string;
|
|
43
|
+
description: string;
|
|
44
|
+
/** Checkpoint tags it reads on (`"*"` = every page). */
|
|
45
|
+
from: string[];
|
|
46
|
+
requires: string[];
|
|
47
|
+
/** Field path -> shape (`"catalog.items": [{ name: "string", price: "number" }]`); absent when `read` needs Mem to build. */
|
|
48
|
+
fields?: Record<string, ReadShape>;
|
|
49
|
+
/** Why `fields` is absent. */
|
|
50
|
+
fieldsError?: string;
|
|
51
|
+
file: string;
|
|
52
|
+
map?: string;
|
|
53
|
+
}
|
|
36
54
|
export interface Manifest {
|
|
37
55
|
version: number;
|
|
38
56
|
project: {
|
|
@@ -43,6 +61,8 @@ export interface Manifest {
|
|
|
43
61
|
checkpoints: string[];
|
|
44
62
|
maps: string[];
|
|
45
63
|
blocks: ManifestBlock[];
|
|
64
|
+
/** What each Checkpoint's page shows, from its Read Blocks (version 2+). */
|
|
65
|
+
readBlocks: ManifestReadBlock[];
|
|
46
66
|
flows: ManifestFlow[];
|
|
47
67
|
skipped: ProjectScan["skipped"];
|
|
48
68
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { scanProject } from "./project.js";
|
|
6
|
-
|
|
6
|
+
import { MemPage } from "../engine/mem/mem.js";
|
|
7
|
+
import { readBlockShape } from "../engine/read/read-block.js";
|
|
8
|
+
export const MANIFEST_VERSION = 2;
|
|
7
9
|
function packageVersion() {
|
|
8
10
|
try {
|
|
9
11
|
return JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "..", "package.json"), "utf8")).version;
|
|
@@ -77,12 +79,36 @@ export function manifestFromScan(scan) {
|
|
|
77
79
|
},
|
|
78
80
|
};
|
|
79
81
|
});
|
|
82
|
+
const readBlocks = scan.readBlocks.map((r) => {
|
|
83
|
+
let shape;
|
|
84
|
+
try {
|
|
85
|
+
shape = { fields: readBlockShape(r.block, new MemPage()) };
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
shape = { fieldsError: err instanceof Error ? err.message : String(err) };
|
|
89
|
+
}
|
|
90
|
+
for (const t of r.block.from)
|
|
91
|
+
if (t !== "*")
|
|
92
|
+
checkpoints.add(t);
|
|
93
|
+
return {
|
|
94
|
+
id: `${r.file}#${r.exportName}`,
|
|
95
|
+
name: r.block.name,
|
|
96
|
+
exportName: r.exportName,
|
|
97
|
+
description: r.block.description,
|
|
98
|
+
from: [...r.block.from],
|
|
99
|
+
requires: r.block.requires.map((k) => k.name),
|
|
100
|
+
...shape,
|
|
101
|
+
file: r.file,
|
|
102
|
+
...(r.map ? { map: r.map } : {}),
|
|
103
|
+
};
|
|
104
|
+
});
|
|
80
105
|
return {
|
|
81
106
|
version: MANIFEST_VERSION,
|
|
82
107
|
project: { name: projectName(scan.projectDir), dir: scan.projectDir, waygraph: packageVersion() },
|
|
83
108
|
checkpoints: [...checkpoints].sort(),
|
|
84
109
|
maps: [...maps].sort(),
|
|
85
110
|
blocks,
|
|
111
|
+
readBlocks,
|
|
86
112
|
flows,
|
|
87
113
|
skipped: scan.skipped,
|
|
88
114
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BlocksSelect } from "./select.js";
|
|
2
|
+
import type { ReadBlock } from "../engine/read/read-block.js";
|
|
2
3
|
import type { AnyBlock } from "./scan.js";
|
|
3
|
-
|
|
4
|
+
import { type BlockKind } from "../engine/block/kind.js";
|
|
4
5
|
export interface ScannedBlock {
|
|
5
6
|
block: AnyBlock;
|
|
6
7
|
name: string;
|
|
@@ -34,9 +35,17 @@ export interface BlockEntry {
|
|
|
34
35
|
kind: "nav" | "action";
|
|
35
36
|
description: string;
|
|
36
37
|
}
|
|
38
|
+
/** A Read Block found in a scanned file (beta). */
|
|
39
|
+
export interface ScannedReadBlock {
|
|
40
|
+
block: ReadBlock;
|
|
41
|
+
exportName: string;
|
|
42
|
+
file: string;
|
|
43
|
+
map?: string;
|
|
44
|
+
}
|
|
37
45
|
export interface ProjectScan {
|
|
38
46
|
projectDir: string;
|
|
39
47
|
blocks: ScannedBlock[];
|
|
48
|
+
readBlocks: ScannedReadBlock[];
|
|
40
49
|
flows: ScannedFlow[];
|
|
41
50
|
skipped: {
|
|
42
51
|
block: string;
|
package/dist/manifest/project.js
CHANGED
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
import { matchBlocksSelect } from "./select.js";
|
|
4
4
|
import { exploreRoots, injectTaggedFile } from "./roots.js";
|
|
5
|
-
import {
|
|
5
|
+
import { importFresh, isBlockLike, isFlowLike, isReadBlockLike, walkDir } from "./scan.js";
|
|
6
6
|
import { installedMapPackages, mapPackageBlockFiles } from "./maps.js";
|
|
7
|
-
|
|
8
|
-
const kind = blockKind(block);
|
|
9
|
-
if (kind === "nav" || kind === "page" || kind === "assert" || kind === "api")
|
|
10
|
-
return kind;
|
|
11
|
-
const salt = block.__waygraphSalt;
|
|
12
|
-
if (salt === "effect" || salt === "method" || salt === "api")
|
|
13
|
-
return salt;
|
|
14
|
-
return "block";
|
|
15
|
-
}
|
|
7
|
+
import { blockKindOf } from "../engine/block/kind.js";
|
|
16
8
|
/** A file that fails to import is left out of the scan, and said so: a silent skip reads as "nothing there". */
|
|
17
9
|
export function reportImportError(relFile, err) {
|
|
18
10
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -23,6 +15,7 @@ export async function scanProject(projectDir, opts = {}) {
|
|
|
23
15
|
const select = opts.blocksSelect;
|
|
24
16
|
const onError = opts.onImportError ?? reportImportError;
|
|
25
17
|
const blocks = [];
|
|
18
|
+
const readBlocks = [];
|
|
26
19
|
const flows = [];
|
|
27
20
|
const skipped = [];
|
|
28
21
|
const scanBlockFile = async (absFile, rel, file, map) => {
|
|
@@ -35,6 +28,10 @@ export async function scanProject(projectDir, opts = {}) {
|
|
|
35
28
|
return;
|
|
36
29
|
}
|
|
37
30
|
for (const [exportName, exported] of Object.entries(mod)) {
|
|
31
|
+
if (isReadBlockLike(exported)) {
|
|
32
|
+
readBlocks.push({ block: exported, exportName, file, ...(map ? { map } : {}) });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
38
35
|
if (!isBlockLike(exported))
|
|
39
36
|
continue;
|
|
40
37
|
if (select && !matchBlocksSelect(select, { relFile: file, blockName: exported.name }))
|
|
@@ -49,7 +46,7 @@ export async function scanProject(projectDir, opts = {}) {
|
|
|
49
46
|
block: exported,
|
|
50
47
|
name: exported.name,
|
|
51
48
|
exportName,
|
|
52
|
-
kind:
|
|
49
|
+
kind: blockKindOf(exported),
|
|
53
50
|
description: typeof exported.description === "string" ? exported.description : "",
|
|
54
51
|
from: [...from],
|
|
55
52
|
to: [...to],
|
|
@@ -97,5 +94,5 @@ export async function scanProject(projectDir, opts = {}) {
|
|
|
97
94
|
}
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
|
-
return { projectDir, blocks, flows, skipped };
|
|
97
|
+
return { projectDir, blocks, readBlocks, flows, skipped };
|
|
101
98
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Loads a project's Read Blocks (`*.read.block.ts`, its own and its installed map packages'): importing one registers it,
|
|
2
|
+
// so every run can read the pages it lands on. Sessions get them from the full project scan; a run by flow name does not.
|
|
3
|
+
import { relative } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { installedMapPackages, mapPackageBlockFiles } from "./maps.js";
|
|
6
|
+
import { reportImportError } from "./project.js";
|
|
7
|
+
import { walkDir } from "./scan.js";
|
|
8
|
+
const READ_BLOCK = /\.read\.block\.ts$/;
|
|
9
|
+
/** Imports every Read Block file of the project; returns how many loaded. */
|
|
10
|
+
export async function loadReadBlocks(projectDir) {
|
|
11
|
+
const files = [...walkDir(projectDir, READ_BLOCK), ...installedMapPackages(projectDir).flatMap(mapPackageBlockFiles).filter((f) => READ_BLOCK.test(f))];
|
|
12
|
+
let loaded = 0;
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
try {
|
|
15
|
+
await import(pathToFileURL(file).href);
|
|
16
|
+
loaded += 1;
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
reportImportError(relative(projectDir, file), err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return loaded;
|
|
23
|
+
}
|
package/dist/manifest/scan.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Block } from "../engine/block/block.js";
|
|
2
2
|
import type { Checkpoint } from "../engine/checkpoint/checkpoint.js";
|
|
3
|
+
import type { ReadBlock } from "../engine/read/read-block.js";
|
|
3
4
|
/** Every file under `dir` whose name matches, skipping `node_modules`, `dist` and `.git`. */
|
|
4
5
|
export declare function walkDir(dir: string, pattern: RegExp): string[];
|
|
5
6
|
/**
|
|
@@ -11,9 +12,9 @@ export type AnyBlock = Block<Checkpoint<string>, Checkpoint<string>> & {
|
|
|
11
12
|
description?: string;
|
|
12
13
|
};
|
|
13
14
|
export declare function isBlockLike(val: unknown): val is AnyBlock;
|
|
15
|
+
/** A Read Block (`defineReadBlock`): a name, the Checkpoints it reads, and an instruction with only `read`. */
|
|
16
|
+
export declare function isReadBlockLike(val: unknown): val is ReadBlock;
|
|
14
17
|
export declare function isFlowLike(val: unknown): val is {
|
|
15
18
|
run: (...args: unknown[]) => Promise<unknown>;
|
|
16
19
|
blocks: () => unknown[];
|
|
17
20
|
};
|
|
18
|
-
/** The factory kind a Block was built with (`nav`, `page`, `assert`), or undefined for Methods and Effects. */
|
|
19
|
-
export declare function blockKind(block: object): string | undefined;
|
package/dist/manifest/scan.js
CHANGED
|
@@ -45,13 +45,17 @@ export function isBlockLike(val) {
|
|
|
45
45
|
const instruction = obj.instruction;
|
|
46
46
|
return instruction !== null && typeof instruction === "object" && typeof instruction?.act === "function";
|
|
47
47
|
}
|
|
48
|
+
/** A Read Block (`defineReadBlock`): a name, the Checkpoints it reads, and an instruction with only `read`. */
|
|
49
|
+
export function isReadBlockLike(val) {
|
|
50
|
+
if (val === null || typeof val !== "object")
|
|
51
|
+
return false;
|
|
52
|
+
const obj = val;
|
|
53
|
+
const instruction = obj.instruction;
|
|
54
|
+
return typeof obj.name === "string" && Array.isArray(obj.from) && typeof instruction?.read === "function" && instruction.act === undefined;
|
|
55
|
+
}
|
|
48
56
|
export function isFlowLike(val) {
|
|
49
57
|
return (val !== null &&
|
|
50
58
|
typeof val === "object" &&
|
|
51
59
|
typeof val.run === "function" &&
|
|
52
60
|
typeof val.blocks === "function");
|
|
53
61
|
}
|
|
54
|
-
/** The factory kind a Block was built with (`nav`, `page`, `assert`), or undefined for Methods and Effects. */
|
|
55
|
-
export function blockKind(block) {
|
|
56
|
-
return block.__waygraphKind;
|
|
57
|
-
}
|