waygraph 0.16.2 → 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 +43 -1
- 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/usage.js +6 -0
- 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 -0
- 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 +18 -21
- package/dist/demo/overlay/overlay-stage.js +1 -1
- package/dist/demo/overlay/phase-fixtures.js +5 -5
- package/dist/demo/overlay/pilot/shell.js +7 -5
- package/dist/demo/overlay/ui/components/ring.js +2 -2
- package/dist/demo/overlay/ui/overlay.css +98 -35
- package/dist/demo/overlay/ui/shadow.js +69 -2
- package/dist/demo/stepper/demo-log.js +8 -8
- package/dist/demo/stepper/slides.d.ts +1 -1
- package/dist/demo/stepper/slides.js +1 -1
- package/dist/demo/stepper/stepper.js +1 -1
- package/dist/engine/block/kind.d.ts +3 -0
- package/dist/engine/block/kind.js +11 -0
- 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.js +25 -13
- package/dist/engine/stub/stub.d.ts +26 -21
- package/dist/index.d.ts +7 -4
- package/dist/index.js +9 -4
- 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/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/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 +1 -1
- package/examples/saucedemo/src/flows/checkout.flow.ts +11 -11
- package/examples/saucedemo/src/flows/login.flow.ts +1 -1
- package/examples/saucedemo/src/flows/shop.flow.ts +1 -1
- 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 +37 -37
- package/examples/saucedemo/src/stubs/finish-order.ts +2 -2
- package/examples/saucedemo/src/stubs/login.ts +5 -5
- package/package.json +5 -1
- 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 +1 -1
- package/templates/quickstart/src/flows/checkout.flow.ts +11 -11
- package/templates/quickstart/src/flows/login.flow.ts +1 -1
- package/templates/quickstart/src/flows/shop.flow.ts +1 -1
- 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 +37 -37
- package/templates/quickstart/src/stubs/finish-order.ts +2 -2
- 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 +8 -8
- package/templates/skills/waygraph-convention.skill.md +10 -2
- package/templates/skills/waygraph-pilot.skill.md +7 -0
|
@@ -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
|
-
}
|
package/dist/runner/runner.d.ts
CHANGED
package/dist/runner/runner.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Playwright is used) with the project dir and the spec ("cartFlow", "loginFlow then add-item"). Every mode plays the
|
|
3
3
|
// same steps through the same engine; the mode decides what happens around each step.
|
|
4
4
|
import { MemPage } from "../engine/mem/mem.js";
|
|
5
|
-
import {
|
|
5
|
+
import { inRun } from "../engine/run/events.js";
|
|
6
6
|
import { withWebServer } from "../runtime/webserver.js";
|
|
7
|
-
import "../runtime/run-log.js";
|
|
7
|
+
import { installRunLog } from "../runtime/run-log.js";
|
|
8
|
+
import { loadReadBlocks } from "../manifest/read-blocks.js";
|
|
8
9
|
import { planSteps } from "./plan/plan.js";
|
|
9
10
|
import { readOptions, slowMoFor } from "./options.js";
|
|
10
11
|
import { openSession } from "./session.js";
|
|
@@ -16,6 +17,7 @@ import { reportMode } from "./modes/report.js";
|
|
|
16
17
|
export async function play(projectDir, spec) {
|
|
17
18
|
const options = readOptions();
|
|
18
19
|
const mem = new MemPage();
|
|
20
|
+
await loadReadBlocks(projectDir);
|
|
19
21
|
const { steps, flows } = await planSteps(projectDir, spec, mem, options);
|
|
20
22
|
if (options.mode === "branches")
|
|
21
23
|
return playBranches(flows, mem, options);
|
|
@@ -52,12 +54,10 @@ function finish(outcome) {
|
|
|
52
54
|
}
|
|
53
55
|
/** The process entry the CLI's bootstrap calls: one run (and one run log) per invocation. */
|
|
54
56
|
export function startRunner() {
|
|
57
|
+
installRunLog();
|
|
55
58
|
const projectDir = process.argv[2] ?? process.cwd();
|
|
56
59
|
const spec = process.argv[3] ?? "";
|
|
57
|
-
|
|
58
|
-
withWebServer(projectDir, () => play(projectDir, spec))
|
|
59
|
-
.then(() => endRun(opened, {}), (err) => {
|
|
60
|
-
endRun(opened, { error: err });
|
|
60
|
+
inRun(spec, () => withWebServer(projectDir, () => play(projectDir, spec))).catch((err) => {
|
|
61
61
|
console.error(err && err.stack ? err.stack : String(err));
|
|
62
62
|
process.exitCode = 1;
|
|
63
63
|
});
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** How an Engine launches its own browser: the selected config (--config), the Chromium override, the extensions. Called by the package entry. */
|
|
2
|
+
export declare function installFullLaunch(): void;
|
|
@@ -4,31 +4,33 @@ import { chromiumExecutablePath, extensionPathsFromEnv, launchContext } from "./
|
|
|
4
4
|
import { resolveExtensions } from "./extensions.js";
|
|
5
5
|
import { loadWaygraphConfig } from "./waygraph-config.js";
|
|
6
6
|
import { envString } from "../engine/env.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
7
|
+
/** How an Engine launches its own browser: the selected config (--config), the Chromium override, the extensions. Called by the package entry. */
|
|
8
|
+
export function installFullLaunch() {
|
|
9
|
+
setOwnContextLauncher(async (engineConfig) => {
|
|
10
|
+
// A config chosen with `--config` / WAYGRAPH_CONFIG wins over the one the Engine was built with.
|
|
11
|
+
const chosen = envString("WAYGRAPH_CONFIG") ? await loadWaygraphConfig(process.cwd()) : undefined;
|
|
12
|
+
const config = chosen
|
|
13
|
+
? {
|
|
14
|
+
...engineConfig,
|
|
15
|
+
...(chosen.baseUrl !== undefined ? { baseUrl: chosen.baseUrl } : {}),
|
|
16
|
+
...(chosen.extensions !== undefined ? { extensions: chosen.extensions } : {}),
|
|
17
|
+
}
|
|
18
|
+
: engineConfig;
|
|
19
|
+
const browserName = config?.browserName ?? "chromium";
|
|
20
|
+
const executablePath = browserName === "chromium" ? chromiumExecutablePath() : undefined;
|
|
21
|
+
const baseURL = config?.baseUrl ?? envString("WAYGRAPH_BASE_URL");
|
|
22
|
+
return launchContext({
|
|
23
|
+
launcher: resolveLauncher(browserName, config),
|
|
24
|
+
launch: {
|
|
25
|
+
headless: config?.headless ?? true,
|
|
26
|
+
...(config?.slowMo !== undefined ? { slowMo: config.slowMo } : {}),
|
|
27
|
+
...(executablePath ? { executablePath } : {}),
|
|
28
|
+
},
|
|
29
|
+
context: {
|
|
30
|
+
...(baseURL ? { baseURL } : {}),
|
|
31
|
+
...(config?.recordVideo ? { recordVideo: config.recordVideo } : {}),
|
|
32
|
+
},
|
|
33
|
+
extensions: await resolveExtensions(config?.extensions ?? extensionPathsFromEnv(), process.cwd()),
|
|
34
|
+
});
|
|
33
35
|
});
|
|
34
|
-
}
|
|
36
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
+
/** Starts writing run events to the run log. Called by the package entry and the runner; a second call does nothing. */
|
|
2
|
+
export declare function installRunLog(): void;
|
|
1
3
|
/** The folder run logs go to for a project unless the config or the environment says otherwise. */
|
|
2
4
|
export declare function defaultRunLogDir(projectDir: string): string;
|
package/dist/runtime/run-log.js
CHANGED
|
@@ -4,13 +4,20 @@ import { appendFileSync, mkdirSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { onRunEvent } from "../engine/run/events.js";
|
|
6
6
|
import { envString } from "../engine/env.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
let installed = false;
|
|
8
|
+
/** Starts writing run events to the run log. Called by the package entry and the runner; a second call does nothing. */
|
|
9
|
+
export function installRunLog() {
|
|
10
|
+
if (installed)
|
|
10
11
|
return;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
installed = true;
|
|
13
|
+
onRunEvent((event) => {
|
|
14
|
+
const dir = envString("WAYGRAPH_RUN_LOG");
|
|
15
|
+
if (!dir || dir === "0")
|
|
16
|
+
return;
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
appendFileSync(join(dir, `${event.runId}.jsonl`), JSON.stringify(event) + "\n");
|
|
19
|
+
});
|
|
20
|
+
}
|
|
14
21
|
/** The folder run logs go to for a project unless the config or the environment says otherwise. */
|
|
15
22
|
export function defaultRunLogDir(projectDir) {
|
|
16
23
|
return join(projectDir, ".waygraph", "runs");
|
|
@@ -13,7 +13,7 @@ the **reference** for waygraph consumer conventions:
|
|
|
13
13
|
- `defineApiBlock` - a real backend/HTTP check as one step in the same Flow as
|
|
14
14
|
a UI step, with its own live network-activity panel during a demo - see
|
|
15
15
|
`apiDemoFlow` (`src/flows/api-demo.flow.ts`)
|
|
16
|
-
- `ctx.caption()` (bottom subtitle-bar) + `ctx.titlePos()` (banner
|
|
16
|
+
- `ctx.ui.caption()` (bottom subtitle-bar) + `ctx.ui.titlePos()` (banner
|
|
17
17
|
left/center/right) - also on `checkoutFlow`
|
|
18
18
|
|
|
19
19
|
**GitHub Pages walkthrough:**
|
|
@@ -48,7 +48,7 @@ npx playwright install chromium
|
|
|
48
48
|
| **Device showcase** (mobile/tablet + portrait/landscape; narration in `src/stubs/device-showcase.ts`) | `npm run demo:device` |
|
|
49
49
|
| Device showcase + `.webm` | `npm run demo:video` |
|
|
50
50
|
| **Cart bulk** demo (add-all → remove-all) | `npm run demo:bulk` |
|
|
51
|
-
| **Owner checkout** (inline `{ ff: true }`, `ctx.caption()`, banner position) | `npm run demo:checkout` |
|
|
51
|
+
| **Owner checkout** (inline `{ ff: true }`, `ctx.ui.caption()`, banner position) | `npm run demo:checkout` |
|
|
52
52
|
| **FE + BE in one Flow** (`defineApiBlock`, live network panel) | `npm run demo:api` |
|
|
53
53
|
| Record (headless `.webm`) | `npm run record` |
|
|
54
54
|
| List flows | `npm run flows` |
|
|
@@ -9,6 +9,7 @@ src/blocks/
|
|
|
9
9
|
saucedemo-web/ # URL /
|
|
10
10
|
NAV.md
|
|
11
11
|
nav-login.block.ts # Page definePageBlock url:/
|
|
12
|
+
header.read.block.ts # Read defineReadBlock (beta, every page: title, cart count)
|
|
12
13
|
methods/
|
|
13
14
|
fill-username.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
|
|
14
15
|
fill-password.method.block.ts # Method defineMethodBlock (self-loop on LoginPage)
|
|
@@ -18,6 +19,7 @@ src/blocks/
|
|
|
18
19
|
login.sel.ts # LoginSel (not a Block)
|
|
19
20
|
inventory/ # URL /inventory.html
|
|
20
21
|
inventory.page.block.ts # Page definePageBlock
|
|
22
|
+
inventory.read.block.ts # Read defineReadBlock (beta: products, prices, in cart, sort)
|
|
21
23
|
NAV.md
|
|
22
24
|
nav-item-detail.block.ts # Nav defineMemNavBlock (instance row)
|
|
23
25
|
methods/
|
|
@@ -36,6 +38,7 @@ src/blocks/
|
|
|
36
38
|
NAV.md
|
|
37
39
|
cart.sel.ts # CartSel (not a Block)
|
|
38
40
|
nav-cart.block.ts # Nav click header cart
|
|
41
|
+
cart.read.block.ts # Read defineReadBlock (beta: the cart's lines)
|
|
39
42
|
nav-checkout-info.block.ts
|
|
40
43
|
checkout-step-one/ # URL /checkout-step-one.html
|
|
41
44
|
NAV.md
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineReadBlock } from "waygraph/beta";
|
|
2
|
+
import { CartPage } from "../../../states/checkout.states.js";
|
|
3
|
+
import { Cart } from "../../../states/checkout.reads.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Kind: Read (beta)
|
|
7
|
+
* Helper: defineReadBlock
|
|
8
|
+
* Route: saucedemo-web/cart/ (= /cart.html)
|
|
9
|
+
*
|
|
10
|
+
* The cart's lines: name, quantity and price.
|
|
11
|
+
*/
|
|
12
|
+
export const CartReadBlock = defineReadBlock({
|
|
13
|
+
name: "read-cart",
|
|
14
|
+
description: "Each line in the cart: name, quantity, price.",
|
|
15
|
+
from: CartPage,
|
|
16
|
+
instruction: {
|
|
17
|
+
read: (ctx) =>
|
|
18
|
+
Cart({
|
|
19
|
+
lines: ctx.page.list(".cart_item", (row) => ({
|
|
20
|
+
name: row.text(".inventory_item_name"),
|
|
21
|
+
qty: row.number(".cart_quantity"),
|
|
22
|
+
price: row.number(".inventory_item_price"),
|
|
23
|
+
})),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineReadBlock } from "waygraph/beta";
|
|
2
|
+
import { Header } from "../../states/checkout.reads.js";
|
|
3
|
+
import { InventorySel } from "./inventory/methods/inventory-items.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Kind: Read (beta)
|
|
7
|
+
* Helper: defineReadBlock
|
|
8
|
+
* Route: saucedemo-web/ (every signed-in page)
|
|
9
|
+
*
|
|
10
|
+
* The header every page shares: the page title and how many items are in the cart (0 when the badge is not shown).
|
|
11
|
+
*/
|
|
12
|
+
export const HeaderReadBlock = defineReadBlock({
|
|
13
|
+
name: "read-header",
|
|
14
|
+
description: "The page title and the cart count.",
|
|
15
|
+
instruction: {
|
|
16
|
+
read: (ctx) =>
|
|
17
|
+
Header({
|
|
18
|
+
title: ctx.page.text(".title"),
|
|
19
|
+
cartCount: ctx.page.fn(async (page) => {
|
|
20
|
+
const badge = page.locator(InventorySel.cartBadge);
|
|
21
|
+
return (await badge.count()) ? Number(await badge.first().innerText()) : 0;
|
|
22
|
+
}),
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineReadBlock } from "waygraph/beta";
|
|
2
|
+
import { ItemInCart, LoggedIn } from "../../../states/checkout.states.js";
|
|
3
|
+
import { Catalog } from "../../../states/checkout.reads.js";
|
|
4
|
+
import { InventorySel } from "./methods/inventory-items.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Kind: Read (beta)
|
|
8
|
+
* Helper: defineReadBlock
|
|
9
|
+
* Route: saucedemo-web/inventory/ (= /inventory.html)
|
|
10
|
+
*
|
|
11
|
+
* The catalogue as data: every product with its price and whether it is in the cart, and the sort order.
|
|
12
|
+
*/
|
|
13
|
+
export const InventoryReadBlock = defineReadBlock({
|
|
14
|
+
name: "read-inventory",
|
|
15
|
+
description: "Every product, its price and whether it is in the cart; the sort order.",
|
|
16
|
+
from: [LoggedIn, ItemInCart],
|
|
17
|
+
instruction: {
|
|
18
|
+
read: (ctx) =>
|
|
19
|
+
Catalog({
|
|
20
|
+
items: ctx.page.list(InventorySel.card, (row) => ({
|
|
21
|
+
name: row.text(InventorySel.name),
|
|
22
|
+
price: row.number(".inventory_item_price"),
|
|
23
|
+
inCart: row.visible(InventorySel.removeBtnPrefix),
|
|
24
|
+
})),
|
|
25
|
+
sort: ctx.page.value(".product_sort_container"),
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -32,7 +32,7 @@ export const apiDemoEpisode = engine
|
|
|
32
32
|
.start()
|
|
33
33
|
.method(
|
|
34
34
|
CheckBackendHealth.stubBefore((ctx) => {
|
|
35
|
-
ctx.title("Checking backend health · real HTTP request, no browser");
|
|
35
|
+
ctx.ui.title("Checking backend health · real HTTP request, no browser");
|
|
36
36
|
}),
|
|
37
37
|
)
|
|
38
38
|
.end();
|