waygraph 0.15.24 → 0.15.26
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/dist/engine/blocks/effect.d.ts +41 -0
- package/dist/engine/blocks/effect.js +23 -0
- package/dist/engine/blocks/method.d.ts +90 -0
- package/dist/engine/blocks/method.js +70 -0
- package/dist/engine/blocks/nav.d.ts +138 -0
- package/dist/engine/blocks/nav.js +110 -0
- package/dist/engine/blocks/page.d.ts +59 -0
- package/dist/engine/blocks/page.js +91 -0
- package/dist/engine/compose.d.ts +75 -0
- package/dist/engine/compose.js +76 -0
- package/dist/engine/config.d.ts +55 -0
- package/dist/engine/config.js +17 -0
- package/dist/engine/core.d.ts +92 -0
- package/dist/engine/core.js +256 -0
- package/dist/engine/engine-class.d.ts +289 -0
- package/dist/engine/engine-class.js +257 -0
- package/dist/engine/flow.d.ts +266 -0
- package/dist/engine/flow.js +387 -0
- package/dist/engine/locate.d.ts +26 -0
- package/dist/engine/locate.js +62 -0
- package/dist/engine/run-graph.d.ts +147 -0
- package/dist/engine/run-graph.js +223 -0
- package/dist/engine.d.ts +11 -1224
- package/dist/engine.js +12 -1550
- package/dist/highlights/css.d.ts +14 -0
- package/dist/highlights/css.js +58 -0
- package/dist/highlights/demo-pace.d.ts +56 -0
- package/dist/highlights/demo-pace.js +184 -0
- package/dist/highlights/device.d.ts +29 -0
- package/dist/highlights/device.js +171 -0
- package/dist/highlights/merge.d.ts +8 -0
- package/dist/highlights/merge.js +173 -0
- package/dist/highlights/run-phase.d.ts +43 -0
- package/dist/highlights/run-phase.js +211 -0
- package/dist/highlights/stub-ctx.d.ts +10 -0
- package/dist/highlights/stub-ctx.js +281 -0
- package/dist/highlights/style.d.ts +27 -0
- package/dist/highlights/style.js +84 -0
- package/dist/highlights/todo-dock.d.ts +52 -0
- package/dist/highlights/todo-dock.js +248 -0
- package/dist/highlights/types.d.ts +536 -0
- package/dist/highlights/types.js +1 -0
- package/dist/highlights.d.ts +9 -734
- package/dist/highlights.js +10 -1416
- package/examples/saucedemo/.waygraph-auto/008f4e6e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/22fefa13.log +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { BlockInfo } from "./flow.js";
|
|
2
|
+
import type { Block, Checkpoint } from "../types.js";
|
|
3
|
+
import type { Trait } from "../trait.js";
|
|
4
|
+
/**
|
|
5
|
+
* A Block guaranteed to have `withStepVerify`/`modStepVerify` attached - what
|
|
6
|
+
* {@link composeBlock} returns. Extends plain `Block`, so it drops straight into
|
|
7
|
+
* `defineFlow([start, ..., composed, ..., end])` like any other Block; no special
|
|
8
|
+
* casing needed anywhere that only expects a `Block`.
|
|
9
|
+
*/
|
|
10
|
+
export interface ComposedBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> extends Block<In, Out> {
|
|
11
|
+
/**
|
|
12
|
+
* This composed Block's constituent steps, in order, as plain data - same
|
|
13
|
+
* shape and purpose as {@link Flow.blocks}.
|
|
14
|
+
* @example GovFormBlock.steps() // [{ name: "step-1-name" }, { name: "step-2-address" }, ...]
|
|
15
|
+
*/
|
|
16
|
+
steps(): readonly BlockInfo[];
|
|
17
|
+
/**
|
|
18
|
+
* Replaces one step's whole verify list, wherever it sits in this composed
|
|
19
|
+
* Block - same rules as {@link withVerify}. Throws if no step matches.
|
|
20
|
+
* @example GovFormBlock.withStepVerify("step-2-address", [])
|
|
21
|
+
*/
|
|
22
|
+
withStepVerify(step: Block<any, any> | string | number, verify: Trait[] | ((out: any) => Trait[])): ComposedBlock<In, Out>;
|
|
23
|
+
/**
|
|
24
|
+
* Replaces one Trait inside one step's verify list - same rules as
|
|
25
|
+
* {@link modVerify}. Throws if no step matches, or that step has no verify
|
|
26
|
+
* trait at the given name/index.
|
|
27
|
+
* @example GovFormBlock.modStepVerify("step-2-address", "zip-visible", Trait.visible("#zip-confirmed"))
|
|
28
|
+
*/
|
|
29
|
+
modStepVerify(step: Block<any, any> | string | number, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): ComposedBlock<In, Out>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Chains several Blocks into one named, exported unit - what a naturally
|
|
33
|
+
* multi-step feature (a stepper form with several pages, say) wants instead of
|
|
34
|
+
* either one giant Block with no per-step confirmation, or several separately
|
|
35
|
+
* exported Blocks the caller has to wire together by hand every time. Each
|
|
36
|
+
* step keeps its own real `act`/`resolve`/`verify`, chained the same way
|
|
37
|
+
* `connect()` already chains any two Blocks - `composeBlock` is not a new
|
|
38
|
+
* execution mechanism, just `connect()` reduced over the list plus a name and
|
|
39
|
+
* the same step-list closure `Flow` already keeps for its own Block list, so
|
|
40
|
+
* `withStepVerify`/`modStepVerify` can patch one step from outside without the
|
|
41
|
+
* caller needing to hand-rebuild the chain. Resolution is by `.name`, not
|
|
42
|
+
* object identity, so a step reference stays valid even after an earlier patch
|
|
43
|
+
* swapped that slot for a new object - see {@link Flow.withBlockVerify} for
|
|
44
|
+
* the identical guarantee one level up. Each call returns a new composed
|
|
45
|
+
* Block; the original is untouched.
|
|
46
|
+
* @example composeBlock("gov-form", [step1, step2, step3, step4])
|
|
47
|
+
*/
|
|
48
|
+
export declare function composeBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, Out>]): ComposedBlock<In, Out>;
|
|
49
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, Out>]): ComposedBlock<In, Out>;
|
|
50
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, Out>]): ComposedBlock<In, Out>;
|
|
51
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, Out>]): ComposedBlock<In, Out>;
|
|
52
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, Out>]): ComposedBlock<In, Out>;
|
|
53
|
+
export declare function composeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, F>, Block<F, Out>]): ComposedBlock<In, Out>;
|
|
54
|
+
/**
|
|
55
|
+
* Same shape as {@link composeBlock}, marked {@link FastForwardComposedBlock.fastForward}
|
|
56
|
+
* so demo/run/traverse treat it as **one opaque step** that **blitzes wall-clock**
|
|
57
|
+
* (skip overlay theater + no Playwright slowMo on that step's acts) unless
|
|
58
|
+
* `--ff-expand` / `WAYGRAPH_FF_EXPAND=1`. Inner acts still run for real;
|
|
59
|
+
* failures are prefixed `ff-name > …` so the locus names both the FF unit and
|
|
60
|
+
* the inner step. Prefer this for boring or heavy-check prefixes (auth, seed,
|
|
61
|
+
* dashboard settle) — you may put **several** FF units in one Flow with normal
|
|
62
|
+
* Blocks between them.
|
|
63
|
+
* @example fastForwardComposeBlock("ff-owner-auth", [NavLoginBlock, SubmitLoginForFlow])
|
|
64
|
+
*/
|
|
65
|
+
export interface FastForwardComposedBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>> extends ComposedBlock<In, Out> {
|
|
66
|
+
readonly fastForward: true;
|
|
67
|
+
}
|
|
68
|
+
/** True when `block` came from {@link fastForwardComposeBlock}. */
|
|
69
|
+
export declare function isFastForwardBlock(block: Block<any, any>): block is FastForwardComposedBlock<any, any>;
|
|
70
|
+
export declare function fastForwardComposeBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, Out>]): FastForwardComposedBlock<In, Out>;
|
|
71
|
+
export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, Out>]): FastForwardComposedBlock<In, Out>;
|
|
72
|
+
export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, Out>]): FastForwardComposedBlock<In, Out>;
|
|
73
|
+
export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, Out>]): FastForwardComposedBlock<In, Out>;
|
|
74
|
+
export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, Out>]): FastForwardComposedBlock<In, Out>;
|
|
75
|
+
export declare function fastForwardComposeBlock<In extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>, D extends Checkpoint<string>, E extends Checkpoint<string>, F extends Checkpoint<string>, Out extends Checkpoint<string>>(name: string, steps: readonly [Block<In, B>, Block<B, C>, Block<C, D>, Block<D, E>, Block<E, F>, Block<F, Out>]): FastForwardComposedBlock<In, Out>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { connect } from "../types.js";
|
|
2
|
+
import { findBlockIndex, modVerify, withVerify } from "./core.js";
|
|
3
|
+
export function composeBlock(name, steps) {
|
|
4
|
+
if (steps.length === 0) {
|
|
5
|
+
throw new Error(`composeBlock: "${name}" needs at least one step`);
|
|
6
|
+
}
|
|
7
|
+
const chain = steps.reduce((a, b) => connect(a, b));
|
|
8
|
+
return {
|
|
9
|
+
...chain,
|
|
10
|
+
name,
|
|
11
|
+
steps: () => steps.map((b) => ({ name: b.name, block: b, ...(b.routes ? { routes: b.routes } : {}) })),
|
|
12
|
+
withStepVerify(step, verify) {
|
|
13
|
+
const index = findBlockIndex(steps, step);
|
|
14
|
+
const patched = [...steps];
|
|
15
|
+
patched[index] = withVerify(steps[index], verify);
|
|
16
|
+
return composeBlock(name, patched);
|
|
17
|
+
},
|
|
18
|
+
modStepVerify(step, nameOrIndex, newCheck) {
|
|
19
|
+
const index = findBlockIndex(steps, step);
|
|
20
|
+
const patched = [...steps];
|
|
21
|
+
patched[index] = modVerify(steps[index], nameOrIndex, newCheck);
|
|
22
|
+
return composeBlock(name, patched);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** True when `block` came from {@link fastForwardComposeBlock}. */
|
|
27
|
+
export function isFastForwardBlock(block) {
|
|
28
|
+
return (block.fastForward === true &&
|
|
29
|
+
typeof block.steps === "function");
|
|
30
|
+
}
|
|
31
|
+
function wrapFfError(ffName, err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
if (msg.startsWith(`${ffName} > `)) {
|
|
34
|
+
return err instanceof Error ? err : new Error(msg);
|
|
35
|
+
}
|
|
36
|
+
const wrapped = new Error(`${ffName} > ${msg}`);
|
|
37
|
+
if (err instanceof Error && err.stack) {
|
|
38
|
+
wrapped.stack = err.stack;
|
|
39
|
+
}
|
|
40
|
+
return wrapped;
|
|
41
|
+
}
|
|
42
|
+
export function fastForwardComposeBlock(name, steps) {
|
|
43
|
+
if (steps.length === 0) {
|
|
44
|
+
throw new Error(`fastForwardComposeBlock: "${name}" needs at least one step`);
|
|
45
|
+
}
|
|
46
|
+
const base = composeBlock(name, steps);
|
|
47
|
+
const origAct = base.instruction.act.bind(base.instruction);
|
|
48
|
+
const ff = {
|
|
49
|
+
...base,
|
|
50
|
+
fastForward: true,
|
|
51
|
+
instruction: {
|
|
52
|
+
...base.instruction,
|
|
53
|
+
async act(page, input, mem) {
|
|
54
|
+
try {
|
|
55
|
+
await origAct(page, input, mem);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
throw wrapFfError(name, e);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
withStepVerify(step, verify) {
|
|
63
|
+
const index = findBlockIndex(steps, step);
|
|
64
|
+
const patched = [...steps];
|
|
65
|
+
patched[index] = withVerify(steps[index], verify);
|
|
66
|
+
return fastForwardComposeBlock(name, patched);
|
|
67
|
+
},
|
|
68
|
+
modStepVerify(step, nameOrIndex, newCheck) {
|
|
69
|
+
const index = findBlockIndex(steps, step);
|
|
70
|
+
const patched = [...steps];
|
|
71
|
+
patched[index] = modVerify(steps[index], nameOrIndex, newCheck);
|
|
72
|
+
return fastForwardComposeBlock(name, patched);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
return ff;
|
|
76
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BrowserType } from "@playwright/test";
|
|
2
|
+
import type { Layout } from "./run-graph.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves which `BrowserType` actually launches for a `mem`-only run (no
|
|
5
|
+
* caller-supplied context) - `config.browsers[browserName]` if given,
|
|
6
|
+
* otherwise the real `@playwright/test` export. Waygraph is deliberately
|
|
7
|
+
* "just an opinionated Playwright" - it doesn't own browser automation
|
|
8
|
+
* itself, so it shouldn't force every consumer onto the vanilla launcher.
|
|
9
|
+
* A stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
|
|
10
|
+
* plus a stealth plugin) is a drop-in replacement for `chromium`/`firefox`/
|
|
11
|
+
* `webkit` - same `.launch()` shape - so accepting one here costs nothing for
|
|
12
|
+
* a consumer who never sets it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveLauncher(browserName: "chromium" | "firefox" | "webkit", config: EngineConfig | undefined): BrowserType;
|
|
15
|
+
/** Configures how an Engine launches its own browser - only used by `Flow.run(mem)` (no context given); ignored by `Flow.run(context, mem)`, since that context is already launched. */
|
|
16
|
+
export interface EngineConfig {
|
|
17
|
+
/** Default `true`, matching Playwright's own default. */
|
|
18
|
+
headless?: boolean;
|
|
19
|
+
/** Default `"chromium"`. */
|
|
20
|
+
browserName?: "chromium" | "firefox" | "webkit";
|
|
21
|
+
/** Milliseconds Playwright pauses before each operation - for watching a non-headless run with your own eyes, not for real runs. Default `0`. */
|
|
22
|
+
slowMo?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Overrides which `BrowserType` actually launches, per browser name -
|
|
25
|
+
* default is the real `@playwright/test` export for whichever one
|
|
26
|
+
* `browserName` picks. Accepts anything shaped like Playwright's own
|
|
27
|
+
* `chromium`/`firefox`/`webkit` (same `.launch()` signature) - a
|
|
28
|
+
* stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
|
|
29
|
+
* plus a stealth plugin) drops in here unmodified. Puppeteer is a
|
|
30
|
+
* different `Page`/`BrowserContext` shape entirely and isn't supported by
|
|
31
|
+
* this seam.
|
|
32
|
+
* @example new Engine({ browsers: { chromium: stealthChromium } })
|
|
33
|
+
*/
|
|
34
|
+
browsers?: Partial<Record<"chromium" | "firefox" | "webkit", BrowserType>>;
|
|
35
|
+
/**
|
|
36
|
+
* Playwright `recordVideo` - saves a `.webm` when the context closes.
|
|
37
|
+
* Works headless. Lighter than a full clip-engine pipeline (no overlay/ffmpeg).
|
|
38
|
+
* @example new Engine({ recordVideo: { dir: "./videos" } })
|
|
39
|
+
*/
|
|
40
|
+
recordVideo?: {
|
|
41
|
+
dir: string;
|
|
42
|
+
size?: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Cross-cutting, automatically-enforced verify (see {@link defineLayout})
|
|
49
|
+
* - applies to every Flow this Engine defines. Works the same regardless
|
|
50
|
+
* of folder convention (Waygraph Map or freeform "manual mode") since
|
|
51
|
+
* matching is by Checkpoint tag, never by file path.
|
|
52
|
+
* @example new Engine({ layouts: [AppShellLayout] })
|
|
53
|
+
*/
|
|
54
|
+
layouts?: readonly Layout[];
|
|
55
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Split out of the former 2,900-line engine.ts (see src/ARCHITECTURE.md). Behavior unchanged.
|
|
2
|
+
import { chromium, firefox, webkit } from "@playwright/test";
|
|
3
|
+
const LAUNCHERS = { chromium, firefox, webkit };
|
|
4
|
+
/**
|
|
5
|
+
* Resolves which `BrowserType` actually launches for a `mem`-only run (no
|
|
6
|
+
* caller-supplied context) - `config.browsers[browserName]` if given,
|
|
7
|
+
* otherwise the real `@playwright/test` export. Waygraph is deliberately
|
|
8
|
+
* "just an opinionated Playwright" - it doesn't own browser automation
|
|
9
|
+
* itself, so it shouldn't force every consumer onto the vanilla launcher.
|
|
10
|
+
* A stealth-patched or otherwise customized launcher (e.g. `playwright-extra`
|
|
11
|
+
* plus a stealth plugin) is a drop-in replacement for `chromium`/`firefox`/
|
|
12
|
+
* `webkit` - same `.launch()` shape - so accepting one here costs nothing for
|
|
13
|
+
* a consumer who never sets it.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveLauncher(browserName, config) {
|
|
16
|
+
return config?.browsers?.[browserName] ?? LAUNCHERS[browserName];
|
|
17
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ActionPage, Block, Checkpoint, DefinedBlock, Instruction } from "../types.js";
|
|
2
|
+
import { MemPage } from "../mem-page.js";
|
|
3
|
+
import type { Trait } from "../trait.js";
|
|
4
|
+
export declare function findBlockIndex(middle: readonly Block<any, any>[], block: Block<any, any> | string | number): number;
|
|
5
|
+
/**
|
|
6
|
+
* Attaches routing to `block` without touching its act/observe/resolve/verify -
|
|
7
|
+
* `routes` must cover every tag of `block`'s output (a missing route is a compile
|
|
8
|
+
* error), and the returned Block runs exactly as `block` always did, just followed
|
|
9
|
+
* by whichever next Block the resolved tag routes to. `null` for a tag means that
|
|
10
|
+
* tag is terminal - the run stops there, same as a Block with no routing at all.
|
|
11
|
+
* A route pointing back at `block` itself is a self-loop; see `runGraph`'s
|
|
12
|
+
* `maxSteps` for its safety net. The return type is deliberately `Block<In, any>` -
|
|
13
|
+
* a route can lead anywhere, including back through itself, so there is no fixed
|
|
14
|
+
* Out type to express; state the expected outcomes to `runGraph` via `terminals`
|
|
15
|
+
* instead.
|
|
16
|
+
*/
|
|
17
|
+
export declare function branch<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, routes: {
|
|
18
|
+
[K in Out["__state"]]: Block<Extract<Out, Checkpoint<K>>, any> | null;
|
|
19
|
+
}): DefinedBlock<In, any>;
|
|
20
|
+
/**
|
|
21
|
+
* Builds a Block with `withVerify`/`modVerify`/`modVerifyAll` attached as
|
|
22
|
+
* methods - `LoginBlock.withVerify([...])`, discoverable by typing
|
|
23
|
+
* `LoginBlock.` in an editor, not just callable as `withVerify(LoginBlock, ...)`
|
|
24
|
+
* if you already know it exists. A raw object literal is still a perfectly
|
|
25
|
+
* valid Block (every test fixture in this codebase is one) - it just won't have
|
|
26
|
+
* these attached. The methods are thin: they delegate to the identically-named
|
|
27
|
+
* free functions, which remain the one place this logic actually lives.
|
|
28
|
+
*/
|
|
29
|
+
export declare function defineBlock<In extends Checkpoint<string>, Out extends Checkpoint<string>>(base: {
|
|
30
|
+
name: string;
|
|
31
|
+
description?: Block<In, Out>["description"];
|
|
32
|
+
instruction: Omit<Instruction<In, Out, any>, "act"> & {
|
|
33
|
+
act(page: ActionPage, input: In, mem: MemPage): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
next?: Block<In, Out>["next"];
|
|
36
|
+
requires?: Block<In, Out>["requires"];
|
|
37
|
+
routes?: Block<In, Out>["routes"];
|
|
38
|
+
instanceOptions?: Block<In, Out>["instanceOptions"];
|
|
39
|
+
}): DefinedBlock<In, Out>;
|
|
40
|
+
/**
|
|
41
|
+
* Returns a new Block that runs exactly like `block` - same act/observe/resolve,
|
|
42
|
+
* same routing - but with `verify` replaced. For giving one flow stricter (or
|
|
43
|
+
* looser, or just different) confirmation on a Block it didn't author, without
|
|
44
|
+
* that Block needing to have been written as a parameterized factory in
|
|
45
|
+
* anticipation. Same move as `branch()`: decorate an existing Block, don't ask
|
|
46
|
+
* every Block to pre-build its own override mechanism.
|
|
47
|
+
*
|
|
48
|
+
* Wrap the one Block you want to change, in place, wherever it already sits in
|
|
49
|
+
* a `defineFlow([...])` array - every other Block stays untouched:
|
|
50
|
+
* `defineFlow([start, LoginBlock, withVerify(AddToCartBlock, [...]), end])`.
|
|
51
|
+
* There is no separate positional array to keep in sync and no `null`
|
|
52
|
+
* placeholder for "skip this one" - `defineFlow` has no per-slot verify
|
|
53
|
+
* argument at all, only whatever `verify` each Block's own `instruction` (or
|
|
54
|
+
* a `withVerify(...)` wrapping it) carries with it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function withVerify<B extends Block<any, any>>(block: B, verify: Trait[] | ((out: any) => Trait[])): B;
|
|
57
|
+
type StubPhaseFn<Out extends Checkpoint<string>> = import("../highlights.js").HighlightStubPhaseOrFn<Out>;
|
|
58
|
+
/**
|
|
59
|
+
* Attach / chain a stubBefore lifecycle on a Block. Prefer
|
|
60
|
+
* `(ctx) => { ctx.ring(...); const x = ctx.mem?.get(Key); }`.
|
|
61
|
+
* @example FillUsernameBlock.stubBefore((ctx) => { ctx.ring("username", { ... }) })
|
|
62
|
+
*/
|
|
63
|
+
export declare function withStubBefore<B extends Block<any, any>>(block: B, stub: StubPhaseFn<any>): B;
|
|
64
|
+
/** @see {@link withStubBefore} */
|
|
65
|
+
export declare function withStubAfter<B extends Block<any, any>>(block: B, stub: StubPhaseFn<any>): B;
|
|
66
|
+
/** @see {@link withStubBefore} */
|
|
67
|
+
export declare function withStubOnError<B extends Block<any, any>>(block: B, stub: StubPhaseFn<any>): B;
|
|
68
|
+
/**
|
|
69
|
+
* Replaces one Trait inside `block`'s verify list, leaving every other check
|
|
70
|
+
* untouched - the v1/v2/legacy-variant case, where one check needs a different
|
|
71
|
+
* selector or URL pattern but the rest of the Block is identical. Addressed by
|
|
72
|
+
* the Trait's own `name` (recommended - inserting or reordering other checks
|
|
73
|
+
* can't silently retarget it) or, if you genuinely know the position and don't
|
|
74
|
+
* need that safety, a numeric index. Either way a bad address fails loud
|
|
75
|
+
* immediately instead of quietly doing nothing. `newCheck` can be a bare
|
|
76
|
+
* `check` function (keeps the existing name) or a full `Trait` (to rename too).
|
|
77
|
+
* Only works on a flat `verify` array - a function-form `verify` (from
|
|
78
|
+
* `verify: (out) => Trait[]`) has no fixed list to address into, so this
|
|
79
|
+
* throws rather than guess; use `withVerify` to replace it wholesale instead.
|
|
80
|
+
*/
|
|
81
|
+
export declare function modVerify<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, nameOrIndex: string | number, newCheck: Trait["check"] | Trait): DefinedBlock<In, Out>;
|
|
82
|
+
/**
|
|
83
|
+
* `modVerify`, but for several named checks in one call - a v2 variant that
|
|
84
|
+
* differs in five ways from v1 shouldn't need five nested `modVerify(modVerify(
|
|
85
|
+
* modVerify(...)))` calls to track by eye. `patches` keys are plain strings (a
|
|
86
|
+
* Trait's `name`, not a `MemKey` instance), so an ordinary object literal is
|
|
87
|
+
* fine here - unlike `MemPage`, there's no identity-collision reason to need
|
|
88
|
+
* `[name, check]` pairs. Applies each patch in turn via `modVerify`, so the
|
|
89
|
+
* same fail-loud-on-unknown-name behavior holds for every entry.
|
|
90
|
+
*/
|
|
91
|
+
export declare function modVerifyAll<In extends Checkpoint<string>, Out extends Checkpoint<string>>(block: Block<In, Out>, patches: Record<string, Trait["check"] | Trait>): DefinedBlock<In, Out>;
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export function findBlockIndex(middle, block) {
|
|
2
|
+
if (typeof block === "number") {
|
|
3
|
+
if (block < 0 || block >= middle.length) {
|
|
4
|
+
throw new Error(`Flow: no Block at index ${block} in this flow (has ${middle.length})`);
|
|
5
|
+
}
|
|
6
|
+
return block;
|
|
7
|
+
}
|
|
8
|
+
const blockName = typeof block === "string" ? block : block.name;
|
|
9
|
+
const index = middle.findIndex((b) => b.name === blockName);
|
|
10
|
+
if (index === -1) {
|
|
11
|
+
throw new Error(`Flow: no Block named "${blockName}" in this flow (has: ${middle.map((b) => b.name).join(", ")})`);
|
|
12
|
+
}
|
|
13
|
+
return index;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attaches routing to `block` without touching its act/observe/resolve/verify -
|
|
17
|
+
* `routes` must cover every tag of `block`'s output (a missing route is a compile
|
|
18
|
+
* error), and the returned Block runs exactly as `block` always did, just followed
|
|
19
|
+
* by whichever next Block the resolved tag routes to. `null` for a tag means that
|
|
20
|
+
* tag is terminal - the run stops there, same as a Block with no routing at all.
|
|
21
|
+
* A route pointing back at `block` itself is a self-loop; see `runGraph`'s
|
|
22
|
+
* `maxSteps` for its safety net. The return type is deliberately `Block<In, any>` -
|
|
23
|
+
* a route can lead anywhere, including back through itself, so there is no fixed
|
|
24
|
+
* Out type to express; state the expected outcomes to `runGraph` via `terminals`
|
|
25
|
+
* instead.
|
|
26
|
+
*/
|
|
27
|
+
export function branch(block, routes) {
|
|
28
|
+
const base = stripMethods(block);
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
let routed;
|
|
31
|
+
const routesAsData = {};
|
|
32
|
+
for (const [tag, target] of Object.entries(routes)) {
|
|
33
|
+
routesAsData[tag] = target === null ? null : target === block ? base.name : target.name;
|
|
34
|
+
}
|
|
35
|
+
routed = defineBlock({
|
|
36
|
+
...base,
|
|
37
|
+
next: (checkpoint) => {
|
|
38
|
+
const target = routes[checkpoint.__state];
|
|
39
|
+
if (target === undefined || target === null)
|
|
40
|
+
return undefined;
|
|
41
|
+
// A route naming `block` itself (the only self-reference in scope when
|
|
42
|
+
// `routes` was written) means "loop back with routing intact" - substitute
|
|
43
|
+
// the routed Block, not the original with no `next` of its own.
|
|
44
|
+
return target === block ? routed : target;
|
|
45
|
+
},
|
|
46
|
+
routes: routesAsData,
|
|
47
|
+
});
|
|
48
|
+
return routed;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Strips `withVerify`/`modVerify`/`modVerifyAll` off a Block, keeping everything
|
|
52
|
+
* else - `name`, `instruction`, `next`, `requires`. Every function below that
|
|
53
|
+
* returns a new Block calls this on its input first, then rebuilds through
|
|
54
|
+
* `defineBlock`. Without it, a Block's attached methods close over the shape it
|
|
55
|
+
* had *when they were attached* - call `.withVerify()` on a Block that
|
|
56
|
+
* `branch()` added routing to afterward, and without this step the result would
|
|
57
|
+
* silently drop that routing, since the stale closure never knew about it.
|
|
58
|
+
*/
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
function stripMethods(block) {
|
|
61
|
+
return {
|
|
62
|
+
name: block.name,
|
|
63
|
+
instruction: block.instruction,
|
|
64
|
+
...(block.description ? { description: block.description } : {}),
|
|
65
|
+
...(block.next ? { next: block.next } : {}),
|
|
66
|
+
...(block.requires ? { requires: block.requires } : {}),
|
|
67
|
+
...(block.routes ? { routes: block.routes } : {}),
|
|
68
|
+
...(block.instanceOptions ? { instanceOptions: block.instanceOptions } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Copy non-enumerable waygraph markers so decorate/withVerify keep map() kind checks. */
|
|
72
|
+
function copyWaygraphRuntime(from, to) {
|
|
73
|
+
for (const key of [
|
|
74
|
+
"__waygraphKind",
|
|
75
|
+
"__waygraphSalt",
|
|
76
|
+
"__waygraphNavUrl",
|
|
77
|
+
"__waygraphNavClick",
|
|
78
|
+
]) {
|
|
79
|
+
const desc = Object.getOwnPropertyDescriptor(from, key);
|
|
80
|
+
if (!desc)
|
|
81
|
+
continue;
|
|
82
|
+
// Re-define as configurable so later decorate chains can copy again
|
|
83
|
+
// (factory markers are non-configurable on the original).
|
|
84
|
+
Object.defineProperty(to, key, {
|
|
85
|
+
value: desc.value,
|
|
86
|
+
enumerable: false,
|
|
87
|
+
writable: false,
|
|
88
|
+
configurable: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const ff = from.fastForward;
|
|
92
|
+
if (ff === true) {
|
|
93
|
+
to.fastForward = true;
|
|
94
|
+
}
|
|
95
|
+
const pace = from.demoPace;
|
|
96
|
+
if (pace !== undefined) {
|
|
97
|
+
to.demoPace = pace;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Builds a Block with `withVerify`/`modVerify`/`modVerifyAll` attached as
|
|
102
|
+
* methods - `LoginBlock.withVerify([...])`, discoverable by typing
|
|
103
|
+
* `LoginBlock.` in an editor, not just callable as `withVerify(LoginBlock, ...)`
|
|
104
|
+
* if you already know it exists. A raw object literal is still a perfectly
|
|
105
|
+
* valid Block (every test fixture in this codebase is one) - it just won't have
|
|
106
|
+
* these attached. The methods are thin: they delegate to the identically-named
|
|
107
|
+
* free functions, which remain the one place this logic actually lives.
|
|
108
|
+
*/
|
|
109
|
+
export function defineBlock(base) {
|
|
110
|
+
const plain = {
|
|
111
|
+
name: base.name,
|
|
112
|
+
instruction: base.instruction,
|
|
113
|
+
...(base.description ? { description: base.description } : {}),
|
|
114
|
+
...(base.next ? { next: base.next } : {}),
|
|
115
|
+
...(base.requires ? { requires: base.requires } : {}),
|
|
116
|
+
...(base.routes ? { routes: base.routes } : {}),
|
|
117
|
+
...(base.instanceOptions ? { instanceOptions: base.instanceOptions } : {}),
|
|
118
|
+
};
|
|
119
|
+
// Methods must close over `defined` (this object), not `plain` - factories
|
|
120
|
+
// attach __waygraphKind/__waygraphSalt on the returned object after
|
|
121
|
+
// defineBlock returns; decorate/withVerify need those markers.
|
|
122
|
+
const defined = {
|
|
123
|
+
...plain,
|
|
124
|
+
withVerify: (verify) => withVerify(defined, verify),
|
|
125
|
+
modVerify: (nameOrIndex, newCheck) => modVerify(defined, nameOrIndex, newCheck),
|
|
126
|
+
modVerifyAll: (patches) => modVerifyAll(defined, patches),
|
|
127
|
+
stubBefore: (stub) => withStubBefore(defined, stub),
|
|
128
|
+
stubAfter: (stub) => withStubAfter(defined, stub),
|
|
129
|
+
stubOnError: (stub) => withStubOnError(defined, stub),
|
|
130
|
+
};
|
|
131
|
+
return defined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns a new Block that runs exactly like `block` - same act/observe/resolve,
|
|
135
|
+
* same routing - but with `verify` replaced. For giving one flow stricter (or
|
|
136
|
+
* looser, or just different) confirmation on a Block it didn't author, without
|
|
137
|
+
* that Block needing to have been written as a parameterized factory in
|
|
138
|
+
* anticipation. Same move as `branch()`: decorate an existing Block, don't ask
|
|
139
|
+
* every Block to pre-build its own override mechanism.
|
|
140
|
+
*
|
|
141
|
+
* Wrap the one Block you want to change, in place, wherever it already sits in
|
|
142
|
+
* a `defineFlow([...])` array - every other Block stays untouched:
|
|
143
|
+
* `defineFlow([start, LoginBlock, withVerify(AddToCartBlock, [...]), end])`.
|
|
144
|
+
* There is no separate positional array to keep in sync and no `null`
|
|
145
|
+
* placeholder for "skip this one" - `defineFlow` has no per-slot verify
|
|
146
|
+
* argument at all, only whatever `verify` each Block's own `instruction` (or
|
|
147
|
+
* a `withVerify(...)` wrapping it) carries with it.
|
|
148
|
+
*/
|
|
149
|
+
export function withVerify(block, verify) {
|
|
150
|
+
const base = stripMethods(block);
|
|
151
|
+
const next = defineBlock({ ...base, instruction: { ...base.instruction, verify } });
|
|
152
|
+
copyWaygraphRuntime(block, next);
|
|
153
|
+
return next;
|
|
154
|
+
}
|
|
155
|
+
function isHighlightStubPhase(v) {
|
|
156
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
157
|
+
}
|
|
158
|
+
/** Run a prior stub (fn or slot map) into an open ctx so decorate can chain. */
|
|
159
|
+
async function applyStubIntoCtx(raw, ctx) {
|
|
160
|
+
if (raw === undefined)
|
|
161
|
+
return;
|
|
162
|
+
if (typeof raw === "function") {
|
|
163
|
+
const ret = await raw(ctx);
|
|
164
|
+
if (isHighlightStubPhase(ret)) {
|
|
165
|
+
for (const [id, stub] of Object.entries(ret)) {
|
|
166
|
+
if (stub)
|
|
167
|
+
ctx.ring(id, stub);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (isHighlightStubPhase(raw)) {
|
|
173
|
+
for (const [id, stub] of Object.entries(raw)) {
|
|
174
|
+
if (stub)
|
|
175
|
+
ctx.ring(id, stub);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function withStubPhase(block, phase, stub) {
|
|
180
|
+
const base = stripMethods(block);
|
|
181
|
+
const instr = base.instruction;
|
|
182
|
+
const prev = instr[phase];
|
|
183
|
+
const chained = async (ctx) => {
|
|
184
|
+
await applyStubIntoCtx(prev, ctx);
|
|
185
|
+
await applyStubIntoCtx(stub, ctx);
|
|
186
|
+
};
|
|
187
|
+
const next = defineBlock({
|
|
188
|
+
...base,
|
|
189
|
+
instruction: { ...base.instruction, [phase]: chained },
|
|
190
|
+
});
|
|
191
|
+
copyWaygraphRuntime(block, next);
|
|
192
|
+
return next;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Attach / chain a stubBefore lifecycle on a Block. Prefer
|
|
196
|
+
* `(ctx) => { ctx.ring(...); const x = ctx.mem?.get(Key); }`.
|
|
197
|
+
* @example FillUsernameBlock.stubBefore((ctx) => { ctx.ring("username", { ... }) })
|
|
198
|
+
*/
|
|
199
|
+
export function withStubBefore(block, stub) {
|
|
200
|
+
return withStubPhase(block, "stubBefore", stub);
|
|
201
|
+
}
|
|
202
|
+
/** @see {@link withStubBefore} */
|
|
203
|
+
export function withStubAfter(block, stub) {
|
|
204
|
+
return withStubPhase(block, "stubAfter", stub);
|
|
205
|
+
}
|
|
206
|
+
/** @see {@link withStubBefore} */
|
|
207
|
+
export function withStubOnError(block, stub) {
|
|
208
|
+
return withStubPhase(block, "stubOnError", stub);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Replaces one Trait inside `block`'s verify list, leaving every other check
|
|
212
|
+
* untouched - the v1/v2/legacy-variant case, where one check needs a different
|
|
213
|
+
* selector or URL pattern but the rest of the Block is identical. Addressed by
|
|
214
|
+
* the Trait's own `name` (recommended - inserting or reordering other checks
|
|
215
|
+
* can't silently retarget it) or, if you genuinely know the position and don't
|
|
216
|
+
* need that safety, a numeric index. Either way a bad address fails loud
|
|
217
|
+
* immediately instead of quietly doing nothing. `newCheck` can be a bare
|
|
218
|
+
* `check` function (keeps the existing name) or a full `Trait` (to rename too).
|
|
219
|
+
* Only works on a flat `verify` array - a function-form `verify` (from
|
|
220
|
+
* `verify: (out) => Trait[]`) has no fixed list to address into, so this
|
|
221
|
+
* throws rather than guess; use `withVerify` to replace it wholesale instead.
|
|
222
|
+
*/
|
|
223
|
+
export function modVerify(block, nameOrIndex, newCheck) {
|
|
224
|
+
const verify = block.instruction.verify;
|
|
225
|
+
if (typeof verify === "function") {
|
|
226
|
+
throw new Error(`modVerify: "${block.name}" has a function-form verify, which has no fixed list to address into - use withVerify to replace it wholesale instead`);
|
|
227
|
+
}
|
|
228
|
+
const traits = verify ?? [];
|
|
229
|
+
const index = typeof nameOrIndex === "number" ? nameOrIndex : traits.findIndex((t) => t.name === nameOrIndex);
|
|
230
|
+
const existing = traits[index];
|
|
231
|
+
if (!existing) {
|
|
232
|
+
throw new Error(typeof nameOrIndex === "number"
|
|
233
|
+
? `modVerify: "${block.name}" has no verify trait at index ${nameOrIndex} (has ${traits.length})`
|
|
234
|
+
: `modVerify: "${block.name}" has no verify trait named "${nameOrIndex}" (has: ${traits.map((t) => t.name).join(", ") || "none"})`);
|
|
235
|
+
}
|
|
236
|
+
const replacement = typeof newCheck === "function"
|
|
237
|
+
? { name: typeof nameOrIndex === "string" ? nameOrIndex : existing.name, check: newCheck }
|
|
238
|
+
: newCheck;
|
|
239
|
+
const updated = [...traits];
|
|
240
|
+
updated[index] = replacement;
|
|
241
|
+
return withVerify(block, updated);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* `modVerify`, but for several named checks in one call - a v2 variant that
|
|
245
|
+
* differs in five ways from v1 shouldn't need five nested `modVerify(modVerify(
|
|
246
|
+
* modVerify(...)))` calls to track by eye. `patches` keys are plain strings (a
|
|
247
|
+
* Trait's `name`, not a `MemKey` instance), so an ordinary object literal is
|
|
248
|
+
* fine here - unlike `MemPage`, there's no identity-collision reason to need
|
|
249
|
+
* `[name, check]` pairs. Applies each patch in turn via `modVerify`, so the
|
|
250
|
+
* same fail-loud-on-unknown-name behavior holds for every entry.
|
|
251
|
+
*/
|
|
252
|
+
export function modVerifyAll(block, patches) {
|
|
253
|
+
const seed = defineBlock(stripMethods(block));
|
|
254
|
+
copyWaygraphRuntime(block, seed);
|
|
255
|
+
return Object.entries(patches).reduce((current, [name, newCheck]) => modVerify(current, name, newCheck), seed);
|
|
256
|
+
}
|