waygraph 0.14.0 → 0.15.1
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 +171 -21
- package/dist/cli.js +101 -5
- package/dist/engine.d.ts +161 -0
- package/dist/engine.js +230 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/map-check.d.ts +18 -0
- package/dist/map-check.js +140 -0
- package/dist/types.d.ts +21 -1
- package/dist/types.js +17 -1
- package/examples/routed-demo/.waygraph-auto/4d4902b7.log +0 -0
- package/examples/routed-demo/.waygraph-auto/51ef462e.log +0 -0
- package/examples/routed-demo/.waygraph-auto/71527ad9.log +0 -0
- package/examples/routed-demo/.waygraph-auto/ca16778e.log +0 -0
- package/examples/routed-demo/src/flows/routed-demo-map.flow.ts +39 -0
- package/examples/routed-demo/src/flows/routed-demo.flow.ts +5 -5
- package/examples/routed-demo/src/{routes/(base_app)/dashboard/methods → map/(app_base)/dashboard/_methods}/click-widget.block.ts +2 -2
- package/examples/routed-demo/src/{routes/(base_app)/dashboard/nav.block.ts → map/(app_base)/dashboard/_nav.block.ts} +2 -2
- package/examples/routed-demo/src/{routes/(base_app)/dashboard/page.block.ts → map/(app_base)/dashboard/_page.block.ts} +4 -4
- package/examples/routed-demo/src/{routes/(external)/docs/nav.block.ts → map/(external)/docs/_nav.block.ts} +2 -2
- package/examples/routed-demo/src/{routes/(external)/docs/page.block.ts → map/(external)/docs/_page.block.ts} +1 -1
- package/examples/routed-demo/test-results/.last-run.json +4 -0
- package/examples/routed-demo/tests/routed-demo-map.spec.ts +17 -0
- package/examples/saucedemo/.waygraph-auto/00d5336e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/083cc5b9.log +0 -0
- package/examples/saucedemo/.waygraph-auto/104751ef.log +0 -0
- package/examples/saucedemo/.waygraph-auto/175655f7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/2219f00b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/23e62571.log +0 -0
- package/examples/saucedemo/.waygraph-auto/42cc9113.log +0 -0
- package/examples/saucedemo/.waygraph-auto/439d30aa.log +0 -0
- package/examples/saucedemo/.waygraph-auto/4a85f496.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5c0eef7f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5d60da77.log +0 -0
- package/examples/saucedemo/.waygraph-auto/5fc3a33b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/61b6546d.log +0 -0
- package/examples/saucedemo/.waygraph-auto/650ed066.log +0 -0
- package/examples/saucedemo/.waygraph-auto/6d9aaad3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/78b21219.log +0 -0
- package/examples/saucedemo/.waygraph-auto/84d6d293.log +0 -0
- package/examples/saucedemo/.waygraph-auto/8a3eb088.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a2dd30dd.log +0 -0
- package/examples/saucedemo/.waygraph-auto/a87bb3ee.log +0 -0
- package/examples/saucedemo/.waygraph-auto/b81e074f.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ba58abb7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bb9fd384.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c02ea583.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c2caf0a8.log +0 -0
- package/examples/saucedemo/.waygraph-auto/c3bdc714.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cd68bbc4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/cdc6db50.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d01906a7.log +0 -0
- package/examples/saucedemo/.waygraph-auto/d9e5f8a4.log +0 -0
- package/examples/saucedemo/.waygraph-auto/dec3076e.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e1973991.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e8306a4b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/ec88b3ef.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f34c9a26.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f5525d53.log +0 -0
- package/examples/saucedemo/.waygraph-auto/f90035b3.log +0 -0
- package/examples/saucedemo/.waygraph-auto/fa687bc2.log +0 -0
- package/examples/saucedemo/.waygraph-auto/fb66fba0.log +0 -0
- package/examples/saucedemo/.waygraph-auto/fc48fd76.log +0 -0
- package/package.json +1 -1
- package/templates/scaffold/scripts/fixture-server.mjs +11 -2
- package/templates/scaffold/src/flows/example.flow.ts +8 -6
- package/templates/scaffold/src/flows/mail-verify.flow.ts +8 -8
- package/templates/scaffold/src/flows/shop.flow.ts +6 -6
- package/templates/scaffold/src/{routes/(external)/docs/nav.block.ts → map/(app_base)/docs/_nav.block.ts} +5 -6
- package/templates/scaffold/src/{routes/(external)/docs/page.block.ts → map/(app_base)/docs/_page.block.ts} +2 -2
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/add-item.effect.block.ts +3 -3
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/assert-hello.method.block.ts +1 -1
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/assert-item-added.method.block.ts +3 -3
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/clear-cart.method.block.ts +9 -3
- package/templates/scaffold/src/{blocks/demo-web/methods → map/(app_base)/home/_methods}/remove-item.effect.block.ts +3 -3
- package/templates/scaffold/src/{blocks/demo-web/nav-home.block.ts → map/(app_base)/home/_nav.block.ts} +2 -2
- package/templates/scaffold/src/{blocks/demo-web/home.page.block.ts → map/(app_base)/home/_page.block.ts} +7 -7
- package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/home}/nav-verification-link.block.ts +4 -4
- package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/assert-email-content.method.block.ts +2 -2
- package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/assert-email-received.method.block.ts +2 -2
- package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/extract-email-link.method.block.ts +2 -2
- package/templates/scaffold/src/{blocks/demo-external/mailpit/methods → map/(external)/mailpit/_methods}/open-message.method.block.ts +2 -2
- package/templates/scaffold/src/{blocks/demo-external/mailpit/nav-mailpit-inbox.block.ts → map/(external)/mailpit/_nav.block.ts} +2 -2
- package/templates/scaffold/src/{blocks/demo-external/mailpit/mailpit-inbox.page.block.ts → map/(external)/mailpit/_page.block.ts} +5 -5
- package/templates/scaffold/src/states/demo.states.ts +2 -4
- package/templates/scaffold/tests/example.spec.ts +2 -2
- package/templates/scaffold/tests/mail-verify.spec.ts +1 -1
- package/templates/scaffold/src/blocks/SITE-MAP.md +0 -12
- package/templates/scaffold/src/blocks/demo-web/NAV.md +0 -29
- package/templates/scaffold/src/flows/routes-demo.flow.ts +0 -14
- /package/examples/routed-demo/src/{routes/(base_app)/dashboard/dashboard.sel.ts → map/(app_base)/dashboard/_sel.ts} +0 -0
- /package/examples/routed-demo/src/{routes/(external)/docs/docs.sel.ts → map/(external)/docs/_sel.ts} +0 -0
- /package/templates/scaffold/src/{routes/(external)/docs/docs.sel.ts → map/(app_base)/docs/_sel.ts} +0 -0
- /package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/docs}/docs.html +0 -0
- /package/templates/scaffold/src/{blocks/demo-web/demo-sel.ts → map/(app_base)/home/_sel.ts} +0 -0
- /package/templates/scaffold/src/{blocks/demo-web → map/(app_base)/home}/home.html +0 -0
- /package/templates/scaffold/src/{blocks/demo-external/mailpit/mailpit-sel.ts → map/(external)/mailpit/_sel.ts} +0 -0
package/dist/engine.js
CHANGED
|
@@ -34,6 +34,46 @@ export function preflight(mem, block) {
|
|
|
34
34
|
.join(", ")}`);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* @example
|
|
39
|
+
* const AppShellLayout = defineLayout({
|
|
40
|
+
* name: "app-shell",
|
|
41
|
+
* appliesTo: ["AppHome", "Chats", "Notifications", "Settings"],
|
|
42
|
+
* verify: [Trait.visible(SidebarSel.root)],
|
|
43
|
+
* });
|
|
44
|
+
* const engine = new Engine({ layouts: [AppShellLayout] });
|
|
45
|
+
* // Now every Block resolving to one of those four Checkpoints, in any
|
|
46
|
+
* // Flow this engine defines, also gets AppShellLayout's own verify run
|
|
47
|
+
* // automatically - a page that silently drops the sidebar fails loud,
|
|
48
|
+
* // even though no individual Block's own `verify` mentions it.
|
|
49
|
+
*/
|
|
50
|
+
export function defineLayout(options) {
|
|
51
|
+
const appliesTo = typeof options.appliesTo === "function"
|
|
52
|
+
? options.appliesTo
|
|
53
|
+
: (tag) => options.appliesTo.includes(tag);
|
|
54
|
+
return {
|
|
55
|
+
name: options.name,
|
|
56
|
+
...(options.description ? { description: options.description } : {}),
|
|
57
|
+
appliesTo,
|
|
58
|
+
verify: options.verify,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Runs every registered layout whose `appliesTo(tag)` matches the just-
|
|
63
|
+
* resolved Checkpoint, in registration order, right after the Block's own
|
|
64
|
+
* `verify` already ran. A layout's own Trait failing throws the same way
|
|
65
|
+
* any other `runVerify` failure does, naming the layout so it's clearly
|
|
66
|
+
* distinguishable from the Block's own verify in the error message.
|
|
67
|
+
*/
|
|
68
|
+
async function runLayouts(layouts, tag, out, page, mem, blockName) {
|
|
69
|
+
if (!layouts)
|
|
70
|
+
return;
|
|
71
|
+
for (const layout of layouts) {
|
|
72
|
+
if (!layout.appliesTo(tag))
|
|
73
|
+
continue;
|
|
74
|
+
await runVerify(layout.verify, out, page, mem, `${blockName} (layout: "${layout.name}")`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
37
77
|
/**
|
|
38
78
|
* Runs `entry` against a fresh tab and returns the resulting terminal Checkpoint.
|
|
39
79
|
* The engine owns the tab's lifecycle, not any individual Block: the tab is
|
|
@@ -72,6 +112,7 @@ export async function runGraph(entry, terminals, context, mem, maxSteps = 5000,
|
|
|
72
112
|
: undefined;
|
|
73
113
|
const checkpoint = await current.instruction.resolve(observed);
|
|
74
114
|
await runVerify(current.instruction.verify, checkpoint, page, mem, current.name);
|
|
115
|
+
await runLayouts(options?.layouts, checkpoint.__state, checkpoint, page, mem, current.name);
|
|
75
116
|
const next = current.next?.(checkpoint);
|
|
76
117
|
if (!next) {
|
|
77
118
|
if (terminals && !terminals.has(checkpoint.__state)) {
|
|
@@ -218,7 +259,11 @@ function buildFlow(middle, engineConfig) {
|
|
|
218
259
|
// withSessionReset()/withTitle() instead (non-destructive decorators,
|
|
219
260
|
// same pattern as withVerify), which also re-apply themselves after any
|
|
220
261
|
// further withBlockVerify/modBlockVerify patch so the flag survives.
|
|
221
|
-
|
|
262
|
+
// Layouts have to be threaded into every pairwise connect(), not just the
|
|
263
|
+
// trailing runGraph call - see connect()'s own doc comment in types.ts:
|
|
264
|
+
// buildFlow's reduce IS the only place a defineFlow-built Flow's
|
|
265
|
+
// intermediate (non-final) Checkpoints are ever verified at all.
|
|
266
|
+
const chain = middle.reduce((a, b) => connect(a, b, engineConfig.layouts));
|
|
222
267
|
// Declared separately (not as the object-literal method below) so the
|
|
223
268
|
// three-overload `Flow.run` signature can be checked against the actual
|
|
224
269
|
// per-call-shape behavior, then attached with a cast - object-literal
|
|
@@ -248,7 +293,9 @@ function buildFlow(middle, engineConfig) {
|
|
|
248
293
|
try {
|
|
249
294
|
const context = await browser.newContext(config.recordVideo ? { recordVideo: config.recordVideo } : {});
|
|
250
295
|
try {
|
|
251
|
-
return await runGraph(chain, undefined, context, mem
|
|
296
|
+
return await runGraph(chain, undefined, context, mem, 5000, {
|
|
297
|
+
...(config.layouts !== undefined ? { layouts: config.layouts } : {}),
|
|
298
|
+
});
|
|
252
299
|
}
|
|
253
300
|
finally {
|
|
254
301
|
// Playwright finalizes recordVideo on context.close(); skip when
|
|
@@ -277,9 +324,11 @@ function buildFlow(middle, engineConfig) {
|
|
|
277
324
|
// call from before this option existed. An explicit `closeOnFinish`
|
|
278
325
|
// always wins either way.
|
|
279
326
|
const closeOnFinish = options?.closeOnFinish ?? gotOwnPage;
|
|
327
|
+
const resolvedLayouts = options?.layouts ?? engineConfig.layouts;
|
|
280
328
|
const result = await runGraph(chain, undefined, context, mem, 5000, {
|
|
281
329
|
page,
|
|
282
330
|
closeOnFinish: false,
|
|
331
|
+
...(resolvedLayouts !== undefined ? { layouts: resolvedLayouts } : {}),
|
|
283
332
|
});
|
|
284
333
|
if (closeOnFinish) {
|
|
285
334
|
await page.close();
|
|
@@ -655,6 +704,185 @@ export class Engine {
|
|
|
655
704
|
defineFlow(blocks) {
|
|
656
705
|
return buildFlow(blocks.slice(1, -1), this.config);
|
|
657
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Fluent, kind-checked alternative to `defineFlow([start, ...blocks, end])` -
|
|
709
|
+
* see {@link MapBuilder}. Real, direct request this responds to: pia/zsign's
|
|
710
|
+
* own agents kept hand-editing/hand-composing Blocks into ad hoc shapes
|
|
711
|
+
* ("locks" convention tried, still got broken) - `map()` forces every step
|
|
712
|
+
* through this Engine's own `define*Block` factories (checked by the same
|
|
713
|
+
* `__waygraphKind`/`__waygraphSalt` runtime markers `graph.ts`/`map-check.ts`
|
|
714
|
+
* already trust), so a hand-rolled plain-object Block can never silently
|
|
715
|
+
* pass as a real navigation/assertion/method step. `homeOrigin`, if given,
|
|
716
|
+
* also gates `.gotoPage()`/`.gotoExternal()` against each Nav/Page Block's
|
|
717
|
+
* own static `url` (skipped, honestly, for click-based/dynamic nav - not
|
|
718
|
+
* statically checkable, same limitation `map-check.ts` already documents).
|
|
719
|
+
* @example
|
|
720
|
+
* const flow = engine.map({ homeOrigin: "https://app.example.com" })
|
|
721
|
+
* .start()
|
|
722
|
+
* .gotoPage(NavHomeBlock)
|
|
723
|
+
* .assert(AssertHelloBlock)
|
|
724
|
+
* .gotoExternal(NavMailpitBlock)
|
|
725
|
+
* .end();
|
|
726
|
+
*/
|
|
727
|
+
map(options) {
|
|
728
|
+
return MapBuilder.begin(this, options?.homeOrigin);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function mapKindOf(block) {
|
|
732
|
+
return block?.__waygraphKind;
|
|
733
|
+
}
|
|
734
|
+
function mapSaltOf(block) {
|
|
735
|
+
return block?.__waygraphSalt;
|
|
736
|
+
}
|
|
737
|
+
function mapNavUrlOf(block) {
|
|
738
|
+
const url = block?.__waygraphNavUrl;
|
|
739
|
+
return typeof url === "string" ? url : undefined;
|
|
740
|
+
}
|
|
741
|
+
const MAP_KIND_FACTORY_HINT = {
|
|
742
|
+
nav: "defineNavBlock/defineMemNavBlock",
|
|
743
|
+
page: "definePageBlock",
|
|
744
|
+
assert: "defineAssertBlock",
|
|
745
|
+
};
|
|
746
|
+
const MAP_SALT_FACTORY_HINT = {
|
|
747
|
+
method: "defineMethodBlock",
|
|
748
|
+
effect: "defineEffectBlock",
|
|
749
|
+
};
|
|
750
|
+
function assertMapKind(block, allowed, method) {
|
|
751
|
+
const kind = mapKindOf(block);
|
|
752
|
+
if (kind !== undefined && allowed.includes(kind))
|
|
753
|
+
return;
|
|
754
|
+
const wanted = allowed.map((k) => MAP_KIND_FACTORY_HINT[k] ?? k).join(" or ");
|
|
755
|
+
const found = kind ? `a Block of kind "${kind}"` : "an object with no waygraph kind marker at all";
|
|
756
|
+
throw new Error(`Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
|
|
757
|
+
`(found ${found} - a hand-built plain object doesn't count). This check is the whole point ` +
|
|
758
|
+
"of the map() builder: only real Blocks from waygraph's own factories can enter a chain.");
|
|
759
|
+
}
|
|
760
|
+
function assertMapSalt(block, allowed, method) {
|
|
761
|
+
const salt = mapSaltOf(block);
|
|
762
|
+
if (salt !== undefined && allowed.includes(salt))
|
|
763
|
+
return;
|
|
764
|
+
const wanted = allowed.map((s) => MAP_SALT_FACTORY_HINT[s] ?? s).join(" or ");
|
|
765
|
+
const found = salt ? `a Block salted "${salt}"` : "an object with no waygraph salt marker at all";
|
|
766
|
+
throw new Error(`Waygraph map: .${method}("${block?.name ?? "?"}") requires a Block built with ${wanted} ` +
|
|
767
|
+
`(found ${found} - a hand-built plain object doesn't count). This check is the whole point ` +
|
|
768
|
+
"of the map() builder: only real Blocks from waygraph's own factories can enter a chain.");
|
|
769
|
+
}
|
|
770
|
+
function assertMapOrigin(block, homeOrigin, expect, method) {
|
|
771
|
+
if (!homeOrigin)
|
|
772
|
+
return; // not configured - can't validate, honest no-op
|
|
773
|
+
const navUrl = mapNavUrlOf(block);
|
|
774
|
+
if (navUrl === undefined)
|
|
775
|
+
return; // click-based/dynamic nav - not statically checkable
|
|
776
|
+
let targetOrigin;
|
|
777
|
+
let wantOrigin;
|
|
778
|
+
try {
|
|
779
|
+
targetOrigin = new URL(navUrl, homeOrigin).origin;
|
|
780
|
+
wantOrigin = new URL(homeOrigin).origin;
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
return; // malformed URL - not this check's job to validate that
|
|
784
|
+
}
|
|
785
|
+
const isExternal = targetOrigin !== wantOrigin;
|
|
786
|
+
if (expect === "internal" && isExternal) {
|
|
787
|
+
throw new Error(`Waygraph map: .gotoPage("${block.name}") targets ${targetOrigin}, which is NOT this map's ` +
|
|
788
|
+
`home origin (${wantOrigin}) - use .gotoExternal() for a genuinely cross-origin destination.`);
|
|
789
|
+
}
|
|
790
|
+
if (expect === "external" && !isExternal) {
|
|
791
|
+
throw new Error(`Waygraph map: .gotoExternal("${block.name}") targets ${targetOrigin}, which IS this map's ` +
|
|
792
|
+
`home origin (${wantOrigin}) - use .gotoPage() for an internal destination.`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Fluent builder over {@link Engine.defineFlow} - see `map()`'s own doc
|
|
797
|
+
* comment for why this exists. Each step method is scoped to exactly the
|
|
798
|
+
* Block kind its name promises, checked at RUNTIME against the
|
|
799
|
+
* `__waygraphKind`/`__waygraphSalt` markers `defineNavBlock`/`definePageBlock`/
|
|
800
|
+
* `defineAssertBlock`/`defineMethodBlock`/`defineEffectBlock` already set
|
|
801
|
+
* (the same markers `graph.ts` and `map-check.ts` trust) - not just a type
|
|
802
|
+
* hint, since a hand-rolled object can satisfy the TypeScript `Block<In,Out>`
|
|
803
|
+
* shape without ever going through a real factory. Each call also
|
|
804
|
+
* typechecks the accumulated Checkpoint chain exactly like `defineFlow`'s
|
|
805
|
+
* own tuple overloads do (a step's `In` must equal the previous step's
|
|
806
|
+
* `Out`) - this is what "no teleporting" means: there is no method on this
|
|
807
|
+
* builder that can skip from one Checkpoint to an unrelated one without a
|
|
808
|
+
* real, kind-correct Block in between.
|
|
809
|
+
*/
|
|
810
|
+
export class MapBuilder {
|
|
811
|
+
engine;
|
|
812
|
+
steps;
|
|
813
|
+
homeOrigin;
|
|
814
|
+
constructor(engine, steps, homeOrigin) {
|
|
815
|
+
this.engine = engine;
|
|
816
|
+
this.steps = steps;
|
|
817
|
+
this.homeOrigin = homeOrigin;
|
|
818
|
+
}
|
|
819
|
+
/** @internal - use `engine.map()` or the standalone `map()` export. */
|
|
820
|
+
static begin(engine, homeOrigin) {
|
|
821
|
+
return new MapBuilder(engine, [], homeOrigin);
|
|
822
|
+
}
|
|
823
|
+
/** Readable chain-opener - mirrors `defineFlow`'s leading `start` sentinel. Returns `this` unchanged; entirely optional. */
|
|
824
|
+
start() {
|
|
825
|
+
return this;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Appends an internal navigation/page-arrival step - requires a Block from
|
|
829
|
+
* `defineNavBlock`/`defineMemNavBlock`/`definePageBlock`. Throws if given
|
|
830
|
+
* anything else, including a `homeOrigin`-mismatched static `url`.
|
|
831
|
+
*/
|
|
832
|
+
gotoPage(block) {
|
|
833
|
+
assertMapKind(block, ["nav", "page"], "gotoPage");
|
|
834
|
+
assertMapOrigin(block, this.homeOrigin, "internal", "gotoPage");
|
|
835
|
+
return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Appends a genuinely cross-origin navigation step (e.g. `(external)/`
|
|
839
|
+
* Blocks in the Waygraph Map convention: mailpit, maildrop.cc) - same
|
|
840
|
+
* kind requirement as {@link gotoPage}, plus the inverse `homeOrigin` check.
|
|
841
|
+
*/
|
|
842
|
+
gotoExternal(block) {
|
|
843
|
+
assertMapKind(block, ["nav", "page"], "gotoExternal");
|
|
844
|
+
assertMapOrigin(block, this.homeOrigin, "external", "gotoExternal");
|
|
845
|
+
return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
|
|
846
|
+
}
|
|
847
|
+
/** Appends a self-loop verification step - requires a Block from `defineAssertBlock`. */
|
|
848
|
+
assert(block) {
|
|
849
|
+
assertMapKind(block, ["assert"], "assert");
|
|
850
|
+
return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Appends a non-navigating page action (submit, upload, add/remove an
|
|
854
|
+
* instance) - requires a Block from `defineMethodBlock`/`defineActionBlock`/
|
|
855
|
+
* `defineEffectBlock`/`defineMemEffectBlock`.
|
|
856
|
+
*/
|
|
857
|
+
method(block) {
|
|
858
|
+
assertMapSalt(block, ["method", "effect"], "method");
|
|
859
|
+
return new MapBuilder(this.engine, [...this.steps, block], this.homeOrigin);
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Finalizes this chain into a real, runnable {@link Flow} - same object
|
|
863
|
+
* `defineFlow` returns, so `withBlockVerify`/`modBlockVerify` and every
|
|
864
|
+
* `withTitle`/`withHighlightFixtures`/etc. decorator still apply exactly
|
|
865
|
+
* as they do today; this builder only changes how the chain is assembled,
|
|
866
|
+
* never what it produces. Throws if no step was ever added - an empty map
|
|
867
|
+
* isn't a flow.
|
|
868
|
+
*/
|
|
869
|
+
end() {
|
|
870
|
+
if (this.steps.length === 0) {
|
|
871
|
+
throw new Error("Waygraph map: .end() called with zero steps - add at least one .gotoPage()/.gotoExternal()/" +
|
|
872
|
+
".assert()/.method() before .end()");
|
|
873
|
+
}
|
|
874
|
+
const chain = [start, ...this.steps, end];
|
|
875
|
+
return this.engine.defineFlow(chain);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Standalone convenience for `new Engine(config).map(options)` - use this
|
|
880
|
+
* when a call site doesn't otherwise need its own `Engine` instance (no
|
|
881
|
+
* shared `headless`/`slowMo`/`layouts` across several flows).
|
|
882
|
+
* @example const flow = map({ homeOrigin: "https://app.example.com" }).gotoPage(NavHomeBlock).end();
|
|
883
|
+
*/
|
|
884
|
+
export function map(options) {
|
|
885
|
+
return new Engine(options).map(options);
|
|
658
886
|
}
|
|
659
887
|
/**
|
|
660
888
|
* Attaches routing to `block` without touching its act/observe/resolve/verify -
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,10 @@ export type { Checkpoint, Instruction, Block, DefinedBlock, Start, WaygraphInsta
|
|
|
2
2
|
export { connect, checkpoint } from "./types.js";
|
|
3
3
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
4
4
|
export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
|
|
5
|
-
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
5
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
|
|
6
6
|
export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
|
|
7
7
|
export type { WaygraphHighlightStub, WaygraphHighlightFixture, WaygraphSlide, WaygraphTodoItem, WaygraphTodoInput, WaygraphTodoGroup, WaygraphTodoGroupInput, WaygraphTodosOpts, TodoDockState, TodoListStyle, FixtureDurationFields, HighlightStubPhase, HighlightStubPhaseName, HighlightFixtureMap, ResolvedHighlight, WaygraphHighlight, HighlightTone, HighlightSize, HighlightWeight, HighlightStyleDefaults, DemoPace, DemoPaceName, StubCtx, StubLifecycleFn, StubPhaseResult, StubPhaseFixtures, DevicePreset, DeviceViewport, DeviceState, DeviceOrientation, } from "./highlights.js";
|
|
8
|
-
export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, } from "./engine.js";
|
|
8
|
+
export type { Flow, EngineConfig, ComposedBlock, FastForwardComposedBlock, RunGraphOptions, BlockInfo, NavBlock, NavBlockOptions, NavClickBlock, NavClickBlockOptions, MethodBlock, ActionBlock, AssertBlockOptions, PageBlock, PageBlockOptions, PageMethodEntry, EffectBlock, MemEffectBlock, MemNavBlock, Layout, LayoutOptions, MapBuilderOptions, } from "./engine.js";
|
|
9
9
|
export type { ActionPage } from "./types.js";
|
|
10
10
|
export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
|
|
11
11
|
export type { WaygraphGraph, WaygraphNode, WaygraphEdge, SkippedBlock, OrphanBlock } from "./graph.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { connect, checkpoint } from "./types.js";
|
|
2
2
|
export { MemKey, MemPage, key, keyGroup } from "./mem-page.js";
|
|
3
3
|
export { Trait, urlMatches, textEquals, visible, frameVisible, frameTextEquals, frameContainsText } from "./trait.js";
|
|
4
|
-
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, } from "./engine.js";
|
|
4
|
+
export { runGraph, spawnTab, narrate, Engine, start, end, branch, withVerify, modVerify, modVerifyAll, defineBlock, defineNavBlock, defineNavClickBlock, defineMethodBlock, defineActionBlock, defineAssertBlock, definePageBlock, defineEffectBlock, defineMemEffectBlock, defineMemNavBlock, locate, composeBlock, fastForwardComposeBlock, isFastForwardBlock, preflight, withSessionReset, withTitle, withDemoPace, withBlockPace, withHighlightStyle, withExpectedFailure, withHighlightFixtures, chainFlow, defineLayout, map, MapBuilder, } from "./engine.js";
|
|
5
5
|
export { resolveHighlightSlots, resolveSlides, runStubPhase, resolveFixtureDwellMs, resolveSlideDwellMs, formatHighlightCaption, hasAuthoredStubAfter, hasAuthoredStubOnError, normalizeHighlightTone, normalizeDemoPace, normalizeHighlightSize, normalizeHighlightWeight, applyHighlightStyleDefaults, toneIconPrefix, resolveStepDemoPace, demoPaceScale, demoPaceIsBlitz, demoPaceIsFast, demoPaceIsSlow, demoPaceGateMs, formatDemoPaceBadge, formatDemoPaceLabel, normalizeTodos, normalizeTodoPos, normalizeTodoStyle, buildTodoDock, applyTodoPhase, advanceTodoDock, completeSequentialTodoDock, applyDevicePhase, resolveDeviceState, normalizeDevicePreset, normalizeDeviceOrientation, applyOrientation, orientationFromViewport, DEVICE_PRESETS, FIXTURE_DURATION_MS_DEFAULT, FIXTURE_DURATION_FAST_MS_DEFAULT, FIXTURE_DURATION_SLOW_MS_DEFAULT, SLIDE_DURATION_MS_DEFAULT, SLIDE_DURATION_FAST_MS_DEFAULT, WAYGRAPH_RING_CSS, } from "./highlights.js";
|
|
6
6
|
export { discoverGraph, toMermaid, findOrphanBlocks, findBlockPath } from "./graph.js";
|
|
7
7
|
export { isFileSelectToken, parseBlocksSelect, matchBlocksSelect, globToRegExp, } from "./blocks-select.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MapViolation {
|
|
2
|
+
file: string;
|
|
3
|
+
block: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
}
|
|
6
|
+
export interface MapNode {
|
|
7
|
+
block: string;
|
|
8
|
+
checkpoint: string | undefined;
|
|
9
|
+
file: string;
|
|
10
|
+
kind: "nav" | "page" | "other";
|
|
11
|
+
}
|
|
12
|
+
export interface MapCheckResult {
|
|
13
|
+
mapRoot: string;
|
|
14
|
+
hasMapDir: boolean;
|
|
15
|
+
nodes: MapNode[];
|
|
16
|
+
violations: MapViolation[];
|
|
17
|
+
}
|
|
18
|
+
export declare function checkMap(projectDir: string): Promise<MapCheckResult>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `waygraph map` - enforcement for the Waygraph Map folder convention.
|
|
3
|
+
* Real, direct user correction: this is not just a naming preference, it's
|
|
4
|
+
* a forced structure. Requires `src/map/` to exist, and for every Nav/Page
|
|
5
|
+
* Block whose `url` is a plain string, the folder path (relative to
|
|
6
|
+
* `src/map/`, with `(group)` segments excluded - those are intentionally
|
|
7
|
+
* invisible in the real URL, same as Next.js route groups) must verbatim
|
|
8
|
+
* match the URL's own real pathname segments, in order. Catches exactly
|
|
9
|
+
* the class of bug a live session found: `(auth)/signin/` grouping a page
|
|
10
|
+
* whose real URL is `/signin`, not `/auth/signin` - a fabricated grouping
|
|
11
|
+
* that never matched the real site.
|
|
12
|
+
*
|
|
13
|
+
* Self-contained (no import from cli.ts), same reasoning as graph.ts's own
|
|
14
|
+
* header comment: cli.ts runs main() unconditionally at module load.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { join, dirname, relative, sep } from "node:path";
|
|
18
|
+
import { pathToFileURL } from "node:url";
|
|
19
|
+
function walkBlockFiles(dir) {
|
|
20
|
+
const results = [];
|
|
21
|
+
let entries;
|
|
22
|
+
try {
|
|
23
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const full = join(dir, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git")
|
|
32
|
+
continue;
|
|
33
|
+
results.push(...walkBlockFiles(full));
|
|
34
|
+
}
|
|
35
|
+
else if (/\.block\.ts$/.test(entry.name)) {
|
|
36
|
+
results.push(full);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
function isBlockLike(val) {
|
|
42
|
+
if (val === null || typeof val !== "object")
|
|
43
|
+
return false;
|
|
44
|
+
const obj = val;
|
|
45
|
+
if (typeof obj.name !== "string")
|
|
46
|
+
return false;
|
|
47
|
+
const instruction = obj.instruction;
|
|
48
|
+
return instruction !== null && typeof instruction === "object" && typeof instruction?.act === "function";
|
|
49
|
+
}
|
|
50
|
+
async function importModule(filePath) {
|
|
51
|
+
const url = pathToFileURL(filePath);
|
|
52
|
+
const mtimeMs = statSync(filePath).mtimeMs;
|
|
53
|
+
url.searchParams.set("t", String(mtimeMs));
|
|
54
|
+
return (await import(url.href));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `(group)` segments never appear in the real URL (Next.js route-group
|
|
58
|
+
* convention this whole system is explicitly modeled on) - excluded from
|
|
59
|
+
* the comparison, not just cosmetically ignored by discoverGraph.
|
|
60
|
+
*
|
|
61
|
+
* A leading-underscore segment (`_methods/`, same as Next.js's own private-
|
|
62
|
+
* folder convention) is the fixed machinery belonging to ONE endpoint - not
|
|
63
|
+
* a route segment of its own - excluded from the comparison for the same
|
|
64
|
+
* reason `(group)` is: real direct request answering "so i have clear
|
|
65
|
+
* distinction of endpoints vs block folders" - a bare (non-underscore,
|
|
66
|
+
* non-parenthesized) folder name always means a real child Checkpoint/URL
|
|
67
|
+
* segment, no exceptions, so `_methods/foo.block.ts` never gets mistaken
|
|
68
|
+
* for a sibling endpoint the way a bare `methods/` folder structurally
|
|
69
|
+
* could be.
|
|
70
|
+
*/
|
|
71
|
+
function realSegments(relPath) {
|
|
72
|
+
return dirname(relPath)
|
|
73
|
+
.split(sep)
|
|
74
|
+
.filter((s) => s !== "." && s !== "" && !/^\(.*\)$/.test(s) && !s.startsWith("_"));
|
|
75
|
+
}
|
|
76
|
+
export async function checkMap(projectDir) {
|
|
77
|
+
const mapRoot = join(projectDir, "src", "map");
|
|
78
|
+
if (!existsSync(mapRoot)) {
|
|
79
|
+
return { mapRoot, hasMapDir: false, nodes: [], violations: [] };
|
|
80
|
+
}
|
|
81
|
+
const files = walkBlockFiles(mapRoot);
|
|
82
|
+
const nodes = [];
|
|
83
|
+
const violations = [];
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const relFile = relative(projectDir, file);
|
|
86
|
+
let mod;
|
|
87
|
+
try {
|
|
88
|
+
mod = await importModule(file);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
violations.push({
|
|
92
|
+
file: relFile,
|
|
93
|
+
block: "(import failed)",
|
|
94
|
+
reason: err instanceof Error ? err.message.split("\n")[0] : String(err),
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
for (const exported of Object.values(mod)) {
|
|
99
|
+
if (!isBlockLike(exported))
|
|
100
|
+
continue;
|
|
101
|
+
const waygraphKind = exported.__waygraphKind;
|
|
102
|
+
const kind = waygraphKind === "nav" ? "nav" : waygraphKind === "page" ? "page" : "other";
|
|
103
|
+
const navUrl = exported.__waygraphNavUrl;
|
|
104
|
+
let checkpoint;
|
|
105
|
+
const instruction = exported.instruction;
|
|
106
|
+
if (instruction?.resolve && typeof instruction.resolve === "function") {
|
|
107
|
+
try {
|
|
108
|
+
const out = (await instruction.resolve(undefined));
|
|
109
|
+
checkpoint = out?.__state;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* resolve() needing real input (a Method Block) - not position-defining, skip */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
nodes.push({ block: exported.name, checkpoint, file: relFile, kind });
|
|
116
|
+
if (typeof navUrl !== "string")
|
|
117
|
+
continue; // only statically-urled Nav/Page Blocks are checkable
|
|
118
|
+
let pathname;
|
|
119
|
+
try {
|
|
120
|
+
pathname = new URL(navUrl).pathname;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
continue; // a relative/templated URL - not verbatim-checkable
|
|
124
|
+
}
|
|
125
|
+
const urlSegments = pathname.split("/").filter(Boolean);
|
|
126
|
+
const folderSegments = realSegments(relative(mapRoot, file));
|
|
127
|
+
const matches = urlSegments.length === folderSegments.length && urlSegments.every((s, i) => s === folderSegments[i]);
|
|
128
|
+
if (!matches) {
|
|
129
|
+
violations.push({
|
|
130
|
+
file: relFile,
|
|
131
|
+
block: exported.name,
|
|
132
|
+
reason: `folder path "${folderSegments.join("/")}" doesn't verbatim-match the real URL path ` +
|
|
133
|
+
`"${urlSegments.join("/")}" (${navUrl}) - (group) segments are excluded from this comparison, ` +
|
|
134
|
+
"everything else must match exactly, same as Next.js file-based routing",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { mapRoot, hasMapDir: true, nodes, violations };
|
|
140
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -277,10 +277,30 @@ export type DefinedBlock<In extends Checkpoint<string>, Out extends Checkpoint<s
|
|
|
277
277
|
modVerify(nameOrIndex: string | number, newCheck: Trait["check"] | Trait): DefinedBlock<In, Out>;
|
|
278
278
|
modVerifyAll(patches: Record<string, Trait["check"] | Trait>): DefinedBlock<In, Out>;
|
|
279
279
|
};
|
|
280
|
+
/**
|
|
281
|
+
* Structural (not imported) twin of `engine.ts`'s `Layout` - `types.ts` sits
|
|
282
|
+
* below `engine.ts` in the module graph, so it can't import `Layout` without
|
|
283
|
+
* a cycle. Same shape, matched by TypeScript's structural typing: any real
|
|
284
|
+
* `Layout` satisfies this with no cast needed at the `connect()` call site.
|
|
285
|
+
*/
|
|
286
|
+
export interface ConnectLayout {
|
|
287
|
+
name: string;
|
|
288
|
+
appliesTo: (tag: string) => boolean;
|
|
289
|
+
verify: Trait[] | ((out: any) => Trait[]);
|
|
290
|
+
}
|
|
280
291
|
/**
|
|
281
292
|
* Composes two Blocks into one, typechecked so this only compiles when A's output
|
|
282
293
|
* tag equals B's input tag. The composed Block's act runs A's full instruction
|
|
283
294
|
* (act -> observe? -> resolve) then hands the resulting Checkpoint into B's act;
|
|
284
295
|
* its own observe/resolve are simply B's, since B is what decides the final Out.
|
|
296
|
+
*
|
|
297
|
+
* `layouts`, if given, also runs against the intermediate Checkpoint (A's
|
|
298
|
+
* resolved output, right before B's act) - the same enforcement `runGraph`'s
|
|
299
|
+
* own loop already applies to a chain's FINAL Checkpoint, but a
|
|
300
|
+
* `defineFlow`-built Flow never reaches that loop per-hop: `buildFlow`
|
|
301
|
+
* reduces the whole Block array into one `connect()`-composed super-Block, so
|
|
302
|
+
* every intermediate hop's verify (and now layout) has to run HERE, inside
|
|
303
|
+
* this recursive `act`, not in `runGraph`'s loop, which only ever sees the
|
|
304
|
+
* one composed Block run start to finish.
|
|
285
305
|
*/
|
|
286
|
-
export declare function connect<A extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>>(a: Block<A, B>, b: Block<B, C
|
|
306
|
+
export declare function connect<A extends Checkpoint<string>, B extends Checkpoint<string>, C extends Checkpoint<string>>(a: Block<A, B>, b: Block<B, C>, layouts?: readonly ConnectLayout[]): Block<A, C>;
|
package/dist/types.js
CHANGED
|
@@ -11,8 +11,17 @@ export function checkpoint(tag) {
|
|
|
11
11
|
* tag equals B's input tag. The composed Block's act runs A's full instruction
|
|
12
12
|
* (act -> observe? -> resolve) then hands the resulting Checkpoint into B's act;
|
|
13
13
|
* its own observe/resolve are simply B's, since B is what decides the final Out.
|
|
14
|
+
*
|
|
15
|
+
* `layouts`, if given, also runs against the intermediate Checkpoint (A's
|
|
16
|
+
* resolved output, right before B's act) - the same enforcement `runGraph`'s
|
|
17
|
+
* own loop already applies to a chain's FINAL Checkpoint, but a
|
|
18
|
+
* `defineFlow`-built Flow never reaches that loop per-hop: `buildFlow`
|
|
19
|
+
* reduces the whole Block array into one `connect()`-composed super-Block, so
|
|
20
|
+
* every intermediate hop's verify (and now layout) has to run HERE, inside
|
|
21
|
+
* this recursive `act`, not in `runGraph`'s loop, which only ever sees the
|
|
22
|
+
* one composed Block run start to finish.
|
|
14
23
|
*/
|
|
15
|
-
export function connect(a, b) {
|
|
24
|
+
export function connect(a, b, layouts) {
|
|
16
25
|
const bObserve = b.instruction.observe;
|
|
17
26
|
const bVerify = b.instruction.verify;
|
|
18
27
|
const requires = [...(a.requires ?? []), ...(b.requires ?? [])];
|
|
@@ -28,6 +37,13 @@ export function connect(a, b) {
|
|
|
28
37
|
: undefined;
|
|
29
38
|
const mid = await a.instruction.resolve(aObserved);
|
|
30
39
|
await runVerify(a.instruction.verify, mid, page, mem, a.name);
|
|
40
|
+
if (layouts) {
|
|
41
|
+
for (const layout of layouts) {
|
|
42
|
+
if (!layout.appliesTo(mid.__state))
|
|
43
|
+
continue;
|
|
44
|
+
await runVerify(layout.verify, mid, page, mem, `${a.name} (layout: "${layout.name}")`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
31
47
|
await runPrecondition(b.instruction.precondition, mid, page, mem, b.name);
|
|
32
48
|
await b.instruction.act(page, mid, mem);
|
|
33
49
|
},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Engine } from "waygraph";
|
|
2
|
+
import { NavDashboardBlock } from "../map/(app_base)/dashboard/_nav.block.js";
|
|
3
|
+
import { ClickWidgetBlock } from "../map/(app_base)/dashboard/_methods/click-widget.block.js";
|
|
4
|
+
import { NavDocsBlock } from "../map/(external)/docs/_nav.block.js";
|
|
5
|
+
// Page hubs registered for auto/docs grouping (arrival-only after nav).
|
|
6
|
+
import "../map/(app_base)/dashboard/_page.block.js";
|
|
7
|
+
import "../map/(external)/docs/_page.block.js";
|
|
8
|
+
|
|
9
|
+
const engine = new Engine();
|
|
10
|
+
|
|
11
|
+
const homeOrigin = process.env.WAYGRAPH_BASE_URL || "http://127.0.0.1:4277";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Same real Blocks, same real Checkpoints, same real fixture server as
|
|
15
|
+
* routed-demo.flow.ts - built with `engine.map()` instead of
|
|
16
|
+
* `engine.defineFlow([start, ..., end])` to prove the fluent builder
|
|
17
|
+
* against a real browser/DOM, not just the in-memory fakes
|
|
18
|
+
* tests/nav/map-builder.spec.ts already covers in the main package.
|
|
19
|
+
*
|
|
20
|
+
* Both nav steps use `.gotoPage()`, not `.gotoExternal()` - even though
|
|
21
|
+
* NavDocsBlock's file lives under the `(external)/` group. That group name
|
|
22
|
+
* is organizational intent (docs.sel.ts's own `WAYGRAPH_DOCS_URL` env
|
|
23
|
+
* override exists for genuinely pointing it at a different origin), but
|
|
24
|
+
* this fixture server happens to serve docs.html from the exact same
|
|
25
|
+
* origin as dashboard.html - the same real inconsistency this session
|
|
26
|
+
* found and fixed in templates/scaffold's own (external)/docs -> (app_base)
|
|
27
|
+
* rename, left as-is here since routed-demo is the one place a reader
|
|
28
|
+
* benefits from seeing it: passing a real `homeOrigin` below means
|
|
29
|
+
* `.gotoExternal(NavDocsBlock)` would actually THROW (same-origin, not
|
|
30
|
+
* cross-origin) - `.gotoPage()` is the one that's honest about what this
|
|
31
|
+
* fixture really does, regardless of the folder it's grouped under.
|
|
32
|
+
*/
|
|
33
|
+
export const routedDemoMapFlow = engine
|
|
34
|
+
.map({ homeOrigin })
|
|
35
|
+
.start()
|
|
36
|
+
.gotoPage(NavDashboardBlock)
|
|
37
|
+
.method(ClickWidgetBlock)
|
|
38
|
+
.gotoPage(NavDocsBlock)
|
|
39
|
+
.end();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Engine, start, end } from "waygraph";
|
|
2
|
-
import { NavDashboardBlock } from "../
|
|
3
|
-
import { ClickWidgetBlock } from "../
|
|
4
|
-
import { NavDocsBlock } from "../
|
|
2
|
+
import { NavDashboardBlock } from "../map/(app_base)/dashboard/_nav.block.js";
|
|
3
|
+
import { ClickWidgetBlock } from "../map/(app_base)/dashboard/_methods/click-widget.block.js";
|
|
4
|
+
import { NavDocsBlock } from "../map/(external)/docs/_nav.block.js";
|
|
5
5
|
// Page hubs registered for auto/docs grouping (arrival-only after nav).
|
|
6
|
-
import "../
|
|
7
|
-
import "../
|
|
6
|
+
import "../map/(app_base)/dashboard/_page.block.js";
|
|
7
|
+
import "../map/(external)/docs/_page.block.js";
|
|
8
8
|
|
|
9
9
|
const engine = new Engine();
|
|
10
10
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineMethodBlock, checkpoint, Trait } from "waygraph";
|
|
2
2
|
import type { Dashboard } from "../../../../states/routed-demo.states.js";
|
|
3
|
-
import { DashboardSel } from "../
|
|
3
|
+
import { DashboardSel } from "../_sel.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Kind: Method
|
|
7
|
-
* Route: (
|
|
7
|
+
* Route: (app_base)/dashboard/_methods/
|
|
8
8
|
*
|
|
9
9
|
* Fixed folder name under the Waygraph Map convention - every page-slug
|
|
10
10
|
* folder's own actions live under methods/, same as today's freeform
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { defineNavBlock, Trait } from "waygraph";
|
|
2
2
|
import type { Dashboard } from "../../../states/routed-demo.states.js";
|
|
3
|
-
import { DashboardSel, DASHBOARD_URL } from "./
|
|
3
|
+
import { DashboardSel, DASHBOARD_URL } from "./_sel.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Kind: Nav
|
|
7
|
-
* Route: (
|
|
7
|
+
* Route: (app_base)/dashboard/
|
|
8
8
|
*
|
|
9
9
|
* Fixed file name under the Waygraph Map convention - every page-slug
|
|
10
10
|
* folder's own navigation Block is always nav.block.ts.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { definePageBlock, Trait } from "waygraph";
|
|
2
2
|
import type { Dashboard } from "../../../states/routed-demo.states.js";
|
|
3
|
-
import { DashboardSel } from "./
|
|
4
|
-
import { ClickWidgetBlock } from "./
|
|
3
|
+
import { DashboardSel } from "./_sel.js";
|
|
4
|
+
import { ClickWidgetBlock } from "./_methods/click-widget.block.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Kind: Page
|
|
8
|
-
* Route: (
|
|
8
|
+
* Route: (app_base)/dashboard/
|
|
9
9
|
*
|
|
10
10
|
* Fixed file name under the Waygraph Map convention - every page-slug
|
|
11
11
|
* folder's own arrival hub is always page.block.ts. Arrival-only
|
|
@@ -13,7 +13,7 @@ import { ClickWidgetBlock } from "./methods/click-widget.block.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export const DashboardPageBlock = definePageBlock<Dashboard>({
|
|
15
15
|
name: "page-dashboard",
|
|
16
|
-
description: "Dashboard hub - the routed-demo example's (
|
|
16
|
+
description: "Dashboard hub - the routed-demo example's (app_base) group.",
|
|
17
17
|
checkpoint: "Dashboard",
|
|
18
18
|
verify: [Trait.visible(DashboardSel.heading)],
|
|
19
19
|
methods: {
|