waygraph 0.15.49 → 0.15.50

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.
@@ -54,7 +54,7 @@ export class AutoSession {
54
54
  this.headless = headless;
55
55
  }
56
56
  static async start(init) {
57
- const baseURL = init.baseURL ?? resolveBaseUrl(init.projectDir);
57
+ const baseURL = init.baseURL ?? (await resolveBaseUrl(init.projectDir));
58
58
  const skipNav = init.skipInitialNavigation === true;
59
59
  const startUrl = skipNav ? undefined : (init.startUrl ?? baseURL);
60
60
  const exploreOpts = {
@@ -1,3 +1,4 @@
1
+ import { resolveBaseUrl } from "../config.js";
1
2
  interface RunFlags {
2
3
  step?: boolean;
3
4
  autoplay?: boolean;
@@ -73,16 +74,18 @@ export declare function applyRunFlags(flags: RunFlags, opts?: {
73
74
  allowDemoUi?: boolean;
74
75
  }): void;
75
76
  /**
76
- * BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set:
77
- * package.json `waygraph.baseUrl` (or `baseURL`), then playwright.config `baseURL`.
77
+ * BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set - see `../config.js`'s own
78
+ * `resolveBaseUrl` for the full resolution order (env / `.env` / `waygraph.config.*` /
79
+ * package.json / playwright.config). Re-exported here so existing importers of this module
80
+ * don't need to change their import path.
78
81
  */
79
- export declare function resolveBaseUrl(projectDir: string): string | undefined;
82
+ export { resolveBaseUrl };
80
83
  /**
81
84
  * Friendly demo defaults: STEP on; headed only for live watch (no --video).
82
85
  * --video / --auto-play-video => headless unless --non-headless.
83
86
  * Call after applyRunFlags so explicit flags already won.
84
87
  */
85
- export declare function applyDemoDefaults(projectDir: string): void;
88
+ export declare function applyDemoDefaults(projectDir: string): Promise<void>;
86
89
  export declare function resolveProjectDir(flags: RunFlags, fallbackIndex?: number): string;
87
90
  export declare function resolveSpec(flags: RunFlags): string | undefined;
88
91
  /** `shop.flow.ts` / `src/flows/cart-bulk.flow.ts` / absolute path — not a Checkpoint or export id. */
@@ -92,4 +95,3 @@ export declare function looksLikeFlowFileRef(ref: string): boolean;
92
95
  * `src/flows/shop.flow.ts` → `shopFlow`; leaves `loginFlow then nav-cart` alone.
93
96
  */
94
97
  export declare function expandSpecFlowFiles(projectDir: string, spec: string): Promise<string>;
95
- export {};
package/dist/cli/flags.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // Split out of the former 8,700-line cli.ts (see src/ARCHITECTURE.md). Behavior unchanged.
2
- import { basename, join, relative, resolve } from "node:path";
2
+ import { basename, relative, resolve } from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { walkDir } from "./discover.js";
5
+ import { resolveBaseUrl } from "../config.js";
5
6
  function takeFlagValue(argv, i, a, flag) {
6
7
  if (a.startsWith(flag + "=")) {
7
8
  return { value: a.slice(flag.length + 1), nextI: i };
@@ -310,42 +311,18 @@ export function applyRunFlags(flags, opts) {
310
311
  }
311
312
  }
312
313
  /**
313
- * BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set:
314
- * package.json `waygraph.baseUrl` (or `baseURL`), then playwright.config `baseURL`.
314
+ * BASE_URL when neither --base-url nor WAYGRAPH_BASE_URL is set - see `../config.js`'s own
315
+ * `resolveBaseUrl` for the full resolution order (env / `.env` / `waygraph.config.*` /
316
+ * package.json / playwright.config). Re-exported here so existing importers of this module
317
+ * don't need to change their import path.
315
318
  */
316
- export function resolveBaseUrl(projectDir) {
317
- const pkgPath = join(projectDir, "package.json");
318
- if (existsSync(pkgPath)) {
319
- try {
320
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
321
- const fromPkg = pkg.waygraph?.baseUrl ??
322
- pkg.waygraph?.baseURL ??
323
- pkg.config?.waygraph?.baseUrl ??
324
- pkg.config?.waygraph?.baseURL;
325
- if (typeof fromPkg === "string" && fromPkg.trim())
326
- return fromPkg.trim();
327
- }
328
- catch {
329
- // ignore malformed package.json - fall through to playwright
330
- }
331
- }
332
- for (const name of ["playwright.config.ts", "playwright.config.mts", "playwright.config.js", "playwright.config.mjs"]) {
333
- const p = join(projectDir, name);
334
- if (!existsSync(p))
335
- continue;
336
- const src = readFileSync(p, "utf-8");
337
- const m = src.match(/baseURL\s*:\s*["']([^"']+)["']/);
338
- if (m?.[1])
339
- return m[1];
340
- }
341
- return undefined;
342
- }
319
+ export { resolveBaseUrl };
343
320
  /**
344
321
  * Friendly demo defaults: STEP on; headed only for live watch (no --video).
345
322
  * --video / --auto-play-video => headless unless --non-headless.
346
323
  * Call after applyRunFlags so explicit flags already won.
347
324
  */
348
- export function applyDemoDefaults(projectDir) {
325
+ export async function applyDemoDefaults(projectDir) {
349
326
  if (process.env.WAYGRAPH_STEP === undefined)
350
327
  process.env.WAYGRAPH_STEP = "1";
351
328
  if (process.env.WAYGRAPH_STEP === "1" && process.env.WAYGRAPH_HEADED === undefined) {
@@ -363,7 +340,7 @@ export function applyDemoDefaults(projectDir) {
363
340
  }
364
341
  }
365
342
  if (!process.env.WAYGRAPH_BASE_URL) {
366
- const resolved = resolveBaseUrl(projectDir);
343
+ const resolved = await resolveBaseUrl(projectDir);
367
344
  if (resolved)
368
345
  process.env.WAYGRAPH_BASE_URL = resolved;
369
346
  }
@@ -41,7 +41,7 @@ mod.startRunner();
41
41
  export async function runChain(projectDir, spec) {
42
42
  const expanded = await expandSpecFlowFiles(projectDir, spec);
43
43
  if (process.env.WAYGRAPH_VIDEO && !process.env.WAYGRAPH_BASE_URL) {
44
- const resolved = resolveBaseUrl(projectDir);
44
+ const resolved = await resolveBaseUrl(projectDir);
45
45
  if (resolved)
46
46
  process.env.WAYGRAPH_BASE_URL = resolved;
47
47
  }
@@ -67,7 +67,7 @@ export async function runChainAuto(projectDir, fromTag, toTag) {
67
67
  const spec = path.join(" then ");
68
68
  console.error(`waygraph chain auto: ${fromTag} -> ${toTag} via ${spec}`);
69
69
  if (!process.env.WAYGRAPH_BASE_URL) {
70
- const resolved = resolveBaseUrl(projectDir);
70
+ const resolved = await resolveBaseUrl(projectDir);
71
71
  if (resolved)
72
72
  process.env.WAYGRAPH_BASE_URL = resolved;
73
73
  }
@@ -24,7 +24,7 @@ export async function runCase(args) {
24
24
  process.exit(1);
25
25
  }
26
26
  if (!process.env.WAYGRAPH_BASE_URL) {
27
- const resolved = resolveBaseUrl(proj);
27
+ const resolved = await resolveBaseUrl(proj);
28
28
  if (resolved)
29
29
  process.env.WAYGRAPH_BASE_URL = resolved;
30
30
  }
@@ -60,13 +60,13 @@ export async function chainCase(args) {
60
60
  }
61
61
  const proj = resolve(flags.blocks ? (flags.positionals[0] ?? process.cwd()) : (flags.positionals[1] ?? process.cwd()));
62
62
  if (!process.env.WAYGRAPH_BASE_URL) {
63
- const resolved = resolveBaseUrl(proj);
63
+ const resolved = await resolveBaseUrl(proj);
64
64
  if (resolved)
65
65
  process.env.WAYGRAPH_BASE_URL = resolved;
66
66
  }
67
67
  // If caller asked for step overlay, behave like demo; else like run.
68
68
  if (flags.step === true || process.env.WAYGRAPH_STEP === "1") {
69
- applyDemoDefaults(proj);
69
+ await applyDemoDefaults(proj);
70
70
  }
71
71
  await runChain(proj, spec);
72
72
  return;
@@ -92,7 +92,7 @@ export async function demoCase(args) {
92
92
  if (flags.step === undefined && process.env.WAYGRAPH_STEP === undefined) {
93
93
  process.env.WAYGRAPH_STEP = "1";
94
94
  }
95
- applyDemoDefaults(proj);
95
+ await applyDemoDefaults(proj);
96
96
  console.error(`waygraph demo: STEP=${process.env.WAYGRAPH_STEP === "1" ? "on" : "off"}` +
97
97
  ` AUTO_NEXT=${process.env.WAYGRAPH_AUTOPLAY === "1" ? "on" : "off"}` +
98
98
  ` HEADED=${process.env.WAYGRAPH_HEADED === "0" ? "off" : "on"}` +
@@ -62,7 +62,7 @@ export async function sessionCase(args, command) {
62
62
  console.error(`waygraph browser: no such directory: ${proj}`);
63
63
  process.exit(1);
64
64
  }
65
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
65
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? (await resolveBaseUrl(proj));
66
66
  try {
67
67
  const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
68
68
  const result = await browserStart({
@@ -148,7 +148,7 @@ export async function sessionCase(args, command) {
148
148
  const flags = parseRunFlags(flagArgs);
149
149
  applyRunFlags(flags);
150
150
  const proj = resolve(flags.positionals[0] ?? process.cwd());
151
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
151
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? (await resolveBaseUrl(proj));
152
152
  try {
153
153
  const result = await pilotStart({
154
154
  projectDir: proj,
@@ -483,7 +483,7 @@ export async function sessionCase(args, command) {
483
483
  process.exit(1);
484
484
  }
485
485
  if (!process.env.WAYGRAPH_BASE_URL) {
486
- const resolved = resolveBaseUrl(proj);
486
+ const resolved = await resolveBaseUrl(proj);
487
487
  if (resolved)
488
488
  process.env.WAYGRAPH_BASE_URL = resolved;
489
489
  }
@@ -510,7 +510,7 @@ export async function sessionCase(args, command) {
510
510
  console.error("waygraph auto --detach requires --cli (headful has no session control)");
511
511
  process.exit(1);
512
512
  }
513
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
513
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? (await resolveBaseUrl(proj));
514
514
  try {
515
515
  const meta = await spawnDetachedSession({
516
516
  projectDir: proj,
@@ -535,7 +535,7 @@ export async function sessionCase(args, command) {
535
535
  }
536
536
  if (flags.blocks && isFileSelectToken(flags.blocks)) {
537
537
  const blocksSelect = parseBlocksSelect(flags.blocks);
538
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
538
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? (await resolveBaseUrl(proj));
539
539
  await runAutoExplore(proj, baseURL
540
540
  ? { cli: cliPicker, baseURL, blocksSelect }
541
541
  : { cli: cliPicker, blocksSelect });
@@ -565,7 +565,7 @@ export async function sessionCase(args, command) {
565
565
  }
566
566
  return;
567
567
  }
568
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
568
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? (await resolveBaseUrl(proj));
569
569
  await runAutoExplore(proj, baseURL ? { cli: cliPicker, baseURL } : { cli: cliPicker });
570
570
  return;
571
571
  }
@@ -578,7 +578,7 @@ export async function autoServeCase(args) {
578
578
  console.error("waygraph __auto-serve: missing --session-id");
579
579
  process.exit(1);
580
580
  }
581
- const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? resolveBaseUrl(proj);
581
+ const baseURL = flags.baseUrl ?? process.env.WAYGRAPH_BASE_URL ?? (await resolveBaseUrl(proj));
582
582
  const inject = flags.inject?.length ? resolveInjectRoots(flags.inject, proj) : undefined;
583
583
  let headless;
584
584
  if (flags.headlessBrowser)
@@ -0,0 +1,34 @@
1
+ export interface WaygraphConfig {
2
+ /** Same value `resolveBaseUrl` would otherwise read from package.json's `waygraph.baseUrl`. */
3
+ baseUrl?: string;
4
+ }
5
+ /** Identity helper, purely for editor type-checking/autocomplete - same role `defineConfig` plays
6
+ * in a `playwright.config.ts` or `vite.config.ts`. `export default defineConfig({ ... })`. */
7
+ export declare function defineConfig(config: WaygraphConfig): WaygraphConfig;
8
+ /**
9
+ * Looks for the first `waygraph.config.*` in `projectDir` and imports it - `.ts` works
10
+ * unmodified because `bin/waygraph` already registers `tsx/esm` before `dist/cli.js` ever runs
11
+ * (the same mechanism that already lets a consumer's `.block.ts`/`.flow.ts` import directly), so
12
+ * no separate loader/transpile step is needed here. A broken config file throws loud, naming the
13
+ * file - silently swallowing it would make a missing `baseUrl` look like "nobody configured
14
+ * anything" instead of "your config file has a bug."
15
+ */
16
+ export declare function loadWaygraphConfig(projectDir: string): Promise<WaygraphConfig | undefined>;
17
+ /**
18
+ * Single source of truth for a project's base URL. Was three independently drifted copies
19
+ * (`src/cli/flags.ts`, `src/traverse/run.ts`, `src/session/auto-explore-run.ts`), each checking
20
+ * a slightly different subset of sources in a different order - real duplication, not just
21
+ * stylistic, since one copy skipped `config.waygraph.*` nesting and another checked
22
+ * `WAYGRAPH_BASE_URL` first instead of last. Checked in this order:
23
+ *
24
+ * 1. `WAYGRAPH_BASE_URL` - a real shell export, or one loaded from a `.env` file (see
25
+ * `loadEnvFiles`, called here so every caller gets native `.env` support for free, not just
26
+ * whichever command remembered to call it).
27
+ * 2. A `waygraph.config.*` file's `baseUrl`.
28
+ * 3. `package.json`'s `waygraph.baseUrl`/`baseURL` (`config.waygraph.*` also checked - a common
29
+ * nested-config convention some tooling uses).
30
+ * 4. `playwright.config.*`'s own `baseURL` (regex-read as text, never imported - importing a
31
+ * consumer's real playwright.config risks pulling in `@playwright/test`'s own global setup
32
+ * as a side effect, which resolving a URL string has no business doing).
33
+ */
34
+ export declare function resolveBaseUrl(projectDir: string): Promise<string | undefined>;
package/dist/config.js ADDED
@@ -0,0 +1,94 @@
1
+ // waygraph.config.* - new, additive, optional. package.json's own "waygraph" field (baseUrl)
2
+ // keeps working untouched; this is a second, richer place to put the same kind of project-level
3
+ // setting, for consumers who'd rather have a real typed file than a JSON field. Real ask: "I
4
+ // think its time to add waygraph config now."
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { loadEnvFiles } from "./env-file.js";
9
+ /** Identity helper, purely for editor type-checking/autocomplete - same role `defineConfig` plays
10
+ * in a `playwright.config.ts` or `vite.config.ts`. `export default defineConfig({ ... })`. */
11
+ export function defineConfig(config) {
12
+ return config;
13
+ }
14
+ const CONFIG_FILENAMES = [
15
+ "waygraph.config.ts",
16
+ "waygraph.config.mts",
17
+ "waygraph.config.mjs",
18
+ "waygraph.config.js",
19
+ "waygraph.config.cjs",
20
+ ];
21
+ /**
22
+ * Looks for the first `waygraph.config.*` in `projectDir` and imports it - `.ts` works
23
+ * unmodified because `bin/waygraph` already registers `tsx/esm` before `dist/cli.js` ever runs
24
+ * (the same mechanism that already lets a consumer's `.block.ts`/`.flow.ts` import directly), so
25
+ * no separate loader/transpile step is needed here. A broken config file throws loud, naming the
26
+ * file - silently swallowing it would make a missing `baseUrl` look like "nobody configured
27
+ * anything" instead of "your config file has a bug."
28
+ */
29
+ export async function loadWaygraphConfig(projectDir) {
30
+ for (const name of CONFIG_FILENAMES) {
31
+ const p = join(projectDir, name);
32
+ if (!existsSync(p))
33
+ continue;
34
+ let mod;
35
+ try {
36
+ mod = (await import(pathToFileURL(p).href));
37
+ }
38
+ catch (err) {
39
+ throw new Error(`waygraph: failed to load ${name}: ${err instanceof Error ? err.message : String(err)}`);
40
+ }
41
+ return mod.default ?? mod;
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Single source of truth for a project's base URL. Was three independently drifted copies
47
+ * (`src/cli/flags.ts`, `src/traverse/run.ts`, `src/session/auto-explore-run.ts`), each checking
48
+ * a slightly different subset of sources in a different order - real duplication, not just
49
+ * stylistic, since one copy skipped `config.waygraph.*` nesting and another checked
50
+ * `WAYGRAPH_BASE_URL` first instead of last. Checked in this order:
51
+ *
52
+ * 1. `WAYGRAPH_BASE_URL` - a real shell export, or one loaded from a `.env` file (see
53
+ * `loadEnvFiles`, called here so every caller gets native `.env` support for free, not just
54
+ * whichever command remembered to call it).
55
+ * 2. A `waygraph.config.*` file's `baseUrl`.
56
+ * 3. `package.json`'s `waygraph.baseUrl`/`baseURL` (`config.waygraph.*` also checked - a common
57
+ * nested-config convention some tooling uses).
58
+ * 4. `playwright.config.*`'s own `baseURL` (regex-read as text, never imported - importing a
59
+ * consumer's real playwright.config risks pulling in `@playwright/test`'s own global setup
60
+ * as a side effect, which resolving a URL string has no business doing).
61
+ */
62
+ export async function resolveBaseUrl(projectDir) {
63
+ loadEnvFiles(projectDir);
64
+ if (process.env.WAYGRAPH_BASE_URL)
65
+ return process.env.WAYGRAPH_BASE_URL;
66
+ const config = await loadWaygraphConfig(projectDir);
67
+ if (config?.baseUrl && config.baseUrl.trim())
68
+ return config.baseUrl.trim();
69
+ const pkgPath = join(projectDir, "package.json");
70
+ if (existsSync(pkgPath)) {
71
+ try {
72
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
73
+ const fromPkg = pkg.waygraph?.baseUrl ??
74
+ pkg.waygraph?.baseURL ??
75
+ pkg.config?.waygraph?.baseUrl ??
76
+ pkg.config?.waygraph?.baseURL;
77
+ if (typeof fromPkg === "string" && fromPkg.trim())
78
+ return fromPkg.trim();
79
+ }
80
+ catch {
81
+ // ignore malformed package.json - fall through to playwright.config
82
+ }
83
+ }
84
+ for (const name of ["playwright.config.ts", "playwright.config.mts", "playwright.config.js", "playwright.config.mjs"]) {
85
+ const p = join(projectDir, name);
86
+ if (!existsSync(p))
87
+ continue;
88
+ const src = readFileSync(p, "utf-8");
89
+ const m = src.match(/baseURL\s*:\s*["']([^"']+)["']/);
90
+ if (m?.[1])
91
+ return m[1];
92
+ }
93
+ return undefined;
94
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Auto-loads `.env` / `.env.local` / `.env.<mode>` / `.env.<mode>.local` from a project's root,
3
+ * same layering Vite/Next use (`mode` defaults to `NODE_ENV`, falling back to "development"),
4
+ * merged into `process.env` in increasing priority. A real, already-set `process.env` value -
5
+ * whatever the shell actually exported - always wins over every file; a `.env` file only fills
6
+ * a gap, it never silently overrides something the caller explicitly set. Safe to call more
7
+ * than once (e.g. once eagerly at `cwd`, again once the real project dir is known) - loading
8
+ * twice is a no-op the second time for any key already filled.
9
+ */
10
+ export declare function loadEnvFiles(projectDir: string, mode?: string): void;
@@ -0,0 +1,58 @@
1
+ // Native .env support - no `dotenv` dependency (same "bring nothing, read what's already there"
2
+ // philosophy as mem-stub.ts's own Faker note: this is a ~20-line parser, not a new runtime dep).
3
+ // Real, direct user ask: FE consumers keep their own .env and had to hack around waygraph not
4
+ // reading it (manually re-exporting values, duplicating them into package.json's "waygraph"
5
+ // field) to get waygraph to see the same config their actual app already uses.
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ /** `KEY=value` lines only - no `${VAR}` expansion, no multiline values. Matches the common
9
+ * Vite/Next `.env` shape, not full dotenv-expand parity. */
10
+ function parseEnvFile(content) {
11
+ const out = {};
12
+ for (const rawLine of content.split(/\r?\n/)) {
13
+ const line = rawLine.trim();
14
+ if (!line || line.startsWith("#"))
15
+ continue;
16
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length) : line;
17
+ const eq = withoutExport.indexOf("=");
18
+ if (eq === -1)
19
+ continue;
20
+ const key = withoutExport.slice(0, eq).trim();
21
+ if (!key)
22
+ continue;
23
+ let value = withoutExport.slice(eq + 1).trim();
24
+ const quoted = (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"));
25
+ if (quoted)
26
+ value = value.slice(1, -1);
27
+ out[key] = value;
28
+ }
29
+ return out;
30
+ }
31
+ /**
32
+ * Auto-loads `.env` / `.env.local` / `.env.<mode>` / `.env.<mode>.local` from a project's root,
33
+ * same layering Vite/Next use (`mode` defaults to `NODE_ENV`, falling back to "development"),
34
+ * merged into `process.env` in increasing priority. A real, already-set `process.env` value -
35
+ * whatever the shell actually exported - always wins over every file; a `.env` file only fills
36
+ * a gap, it never silently overrides something the caller explicitly set. Safe to call more
37
+ * than once (e.g. once eagerly at `cwd`, again once the real project dir is known) - loading
38
+ * twice is a no-op the second time for any key already filled.
39
+ */
40
+ export function loadEnvFiles(projectDir, mode = process.env.NODE_ENV || "development") {
41
+ const files = [".env", ".env.local", `.env.${mode}`, `.env.${mode}.local`];
42
+ for (const name of files) {
43
+ const p = join(projectDir, name);
44
+ if (!existsSync(p))
45
+ continue;
46
+ let parsed;
47
+ try {
48
+ parsed = parseEnvFile(readFileSync(p, "utf-8"));
49
+ }
50
+ catch {
51
+ continue; // unreadable file - never fail a run over an optional .env
52
+ }
53
+ for (const [key, value] of Object.entries(parsed)) {
54
+ if (process.env[key] === undefined)
55
+ process.env[key] = value;
56
+ }
57
+ }
58
+ }
@@ -165,9 +165,9 @@ export type StubPhaseFixtures = {
165
165
  /** Banner UX: collision (move away from a ring it overlaps), hidden. See {@link BannerUiOpts}. */
166
166
  bannerUi?: BannerUiOpts;
167
167
  /**
168
- * Bottom subtitle-bar caption (ported from zsign's help-center-clip-engine) - a full-width,
169
- * bottom-anchored gradient scrim with centered text, distinct from the banner (top, card-shaped).
170
- * Omit/clear to hide it; set again to change the text, fading out/in.
168
+ * Bottom narration caption - a white rounded card (black text, shadow), bottom-centered above
169
+ * the step pill, distinct from the banner (top, card-shaped). Omit/clear to hide it; set again
170
+ * to change the text, fading out/in.
171
171
  */
172
172
  caption?: string;
173
173
  /**
@@ -421,9 +421,9 @@ export type StubCtx<Out extends Checkpoint<string> = Checkpoint<string>> = {
421
421
  */
422
422
  bannerUi(opts: BannerUiOpts): void;
423
423
  /**
424
- * Bottom subtitle-bar caption (ported from zsign's help-center-clip-engine) - a full-width,
425
- * bottom-anchored gradient scrim with centered text; distinct from {@link StubCtx.title}, which
426
- * is the top card. Fades in/out on change. Pass `""` (or call {@link StubCtx.hideCaption}) to clear.
424
+ * Bottom narration caption - a white rounded card (black text, shadow), bottom-centered above
425
+ * the step pill; distinct from {@link StubCtx.title}, which is the top banner card. Fades in/out
426
+ * on change. Pass `""` (or call {@link StubCtx.hideCaption}) to clear.
427
427
  * @example ctx.caption("Click Add to cart to continue");
428
428
  */
429
429
  caption(text: string): void;
package/dist/index.d.ts CHANGED
@@ -29,3 +29,5 @@ export { AutoSession, stubFilePath } from "./auto-session.js";
29
29
  export type { AutoSessionInit, SessionSnapshot, SessionSnapshotEdge, SessionSnapshotSection, ApplyPickResult, ApplyPathResult, TraceStep, ConsoleLogEntry, StorageSnapshot, ServiceWorkerRegistrationInfo, CookieInfo, StubFileKind, } from "./auto-session.js";
30
30
  export { pilotStart } from "./session/pilot.js";
31
31
  export type { PilotStartResult } from "./session/pilot.js";
32
+ export { defineConfig, resolveBaseUrl } from "./config.js";
33
+ export type { WaygraphConfig } from "./config.js";
package/dist/index.js CHANGED
@@ -14,3 +14,4 @@ export { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, form
14
14
  export { WG_UI_SEL, WG_READY_PANEL_SEL, WG_READY_AUTO_SEL, WG_READY_BANNER_SEL, readWgOverlayBeacon, readWgOverlayBeaconInPage, assertWgOverlayReady, } from "./session/overlay-beacon.js";
15
15
  export { AutoSession, stubFilePath } from "./auto-session.js";
16
16
  export { pilotStart } from "./session/pilot.js";
17
+ export { defineConfig, resolveBaseUrl } from "./config.js";
@@ -1,6 +1,6 @@
1
1
  // In-page installer (page.evaluate target) - must stay self-contained, no module-scope closure.
2
- // Bottom subtitle-bar caption, ported from zsign's help-center-clip-engine (see ui/css/caption.css's
3
- // own header) - same visual fixture, narration-timeline/TTS sync not ported yet.
2
+ // Bottom narration caption - a white rounded card, not the dark zsign-ported scrim this started as
3
+ // (see ui/css/caption.css's own header); narration-timeline/TTS sync not ported yet.
4
4
  export function showCaption(text) {
5
5
  let bar = __wgById("wg-caption-bar");
6
6
  if (!bar) {
@@ -3,6 +3,7 @@ import { Engine } from "../core.js";
3
3
  import { MemPage } from "../mem-page.js";
4
4
  import { type BlockEntry } from "./auto-explore.js";
5
5
  import type { Checkpoint } from "../types.js";
6
+ import { resolveBaseUrl } from "../config.js";
6
7
  export interface AutoExploreOptions {
7
8
  /** Terminal menu (browser still runs for act()). Default false = headful picker panel. */
8
9
  cli?: boolean;
@@ -12,7 +13,9 @@ export interface AutoExploreOptions {
12
13
  /** Phase C: glob / regex / bare `--blocks` file select for discovery. */
13
14
  blocksSelect?: import("../blocks-select.js").BlocksSelect;
14
15
  }
15
- export declare function resolveBaseUrl(projectDir: string): string | undefined;
16
+ /** See `../config.js`'s own `resolveBaseUrl` - re-exported so existing importers of this module
17
+ * don't need to change their import path. */
18
+ export { resolveBaseUrl };
16
19
  export declare function detectHere(page: Page, navBlocks: BlockEntry[]): Promise<string | null>;
17
20
  export declare function ensureLivePage(context: BrowserContext, page: Page, startUrl: string | undefined): Promise<Page>;
18
21
  /**
@@ -1,35 +1,14 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { ensureShadowRoot } from "../ui/shadow.js";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
- import { existsSync, readFileSync } from "node:fs";
5
- import { join } from "node:path";
6
4
  import { Engine, start, end, locate } from "../core.js";
7
5
  import { MemPage } from "../mem-page.js";
8
6
  import { buildExploreContext, buildExploreMenu, } from "./auto-explore.js";
9
7
  import { installDemoChrome, instrumentInteractionHighlighting } from "./step-overlay.js";
10
- export function resolveBaseUrl(projectDir) {
11
- const pkgPath = join(projectDir, "package.json");
12
- if (existsSync(pkgPath)) {
13
- try {
14
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
15
- const fromPkg = pkg.waygraph?.baseUrl ?? pkg.waygraph?.baseURL;
16
- if (typeof fromPkg === "string" && fromPkg.trim())
17
- return fromPkg.trim();
18
- }
19
- catch {
20
- /* ignore */
21
- }
22
- }
23
- for (const name of ["playwright.config.ts", "playwright.config.mts", "playwright.config.js"]) {
24
- const p = join(projectDir, name);
25
- if (!existsSync(p))
26
- continue;
27
- const m = readFileSync(p, "utf-8").match(/baseURL\s*:\s*["']([^"']+)["']/);
28
- if (m?.[1])
29
- return m[1];
30
- }
31
- return undefined;
32
- }
8
+ import { resolveBaseUrl } from "../config.js";
9
+ /** See `../config.js`'s own `resolveBaseUrl` - re-exported so existing importers of this module
10
+ * don't need to change their import path. */
11
+ export { resolveBaseUrl };
33
12
  /** URL-first - several Checkpoints share nav verify traits (e.g. LoggedIn = /inventory.html). */
34
13
  async function inferCheckpointFromUrl(page) {
35
14
  try {
@@ -577,7 +556,7 @@ async function cliPick(menu, rl) {
577
556
  export async function runAutoExplore(projectDir, options = {}) {
578
557
  lastRunNote = null;
579
558
  const cli = options.cli === true;
580
- const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
559
+ const baseURL = options.baseURL ?? (await resolveBaseUrl(projectDir)) ?? process.env.WAYGRAPH_BASE_URL;
581
560
  const startUrl = options.startUrl ?? baseURL;
582
561
  const { graph, library } = await buildExploreContext(projectDir, options.blocksSelect ? { blocksSelect: options.blocksSelect } : undefined);
583
562
  if (options.blocksSelect) {
@@ -1,29 +1,10 @@
1
- /**
2
- * Phase B+D: graph traverse (serial or --parallel with --session clone).
3
- * Walk unused legal edges until leaf / budget / break.
4
- */
5
- import { readFileSync, existsSync } from "node:fs";
6
- import { join } from "node:path";
7
1
  import { Engine, start, end, locate } from "../core.js";
8
2
  import { MemPage } from "../mem-page.js";
9
3
  import { buildExploreContext, buildExploreMenu, } from "../session/auto-explore.js";
10
4
  import { defaultMemValueForKey } from "../session/auto-explore-run.js";
5
+ import { resolveBaseUrl } from "../config.js";
11
6
  import { EdgeLeaseCoordinator } from "./lease.js";
12
7
  import { buildCoverageReport, collectGraphEdgeKeys, defaultCoverageOutPath, formatCoverageLine, writeCoverageReport, } from "./coverage.js";
13
- function resolveBaseUrl(projectDir) {
14
- if (process.env.WAYGRAPH_BASE_URL)
15
- return process.env.WAYGRAPH_BASE_URL;
16
- const pkgPath = join(projectDir, "package.json");
17
- if (!existsSync(pkgPath))
18
- return undefined;
19
- try {
20
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
21
- return pkg.waygraph?.baseUrl;
22
- }
23
- catch {
24
- return undefined;
25
- }
26
- }
27
8
  function seedMem(library, mem, dataJson) {
28
9
  const raw = dataJson ?? process.env.WAYGRAPH_DATA ?? process.env.WAYGRAPH_AUTO_MEM;
29
10
  let envParsed = {};
@@ -257,7 +238,7 @@ export async function runTraverse(projectDir, options = {}) {
257
238
  const maxVisitsPerEdge = options.maxVisitsPerEdge ?? 1;
258
239
  const timeoutMs = options.timeoutMs ?? 15 * 60_000;
259
240
  const headed = options.headed === true || process.env.WAYGRAPH_HEADED === "1";
260
- const baseURL = options.baseURL ?? resolveBaseUrl(projectDir) ?? process.env.WAYGRAPH_BASE_URL;
241
+ const baseURL = options.baseURL ?? (await resolveBaseUrl(projectDir)) ?? process.env.WAYGRAPH_BASE_URL;
261
242
  const startUrl = options.startUrl ?? baseURL;
262
243
  const minEdgeCoverage = options.minEdgeCoverage != null && Number.isFinite(options.minEdgeCoverage)
263
244
  ? options.minEdgeCoverage
@@ -0,0 +1,22 @@
1
+ [16:29:47.576] waygraph pilot: stub.before ctx.title("Signing in · mobile")
2
+ [16:29:47.576] waygraph pilot: stub.before ctx.todos n=6 index=0
3
+ [16:29:47.576] waygraph pilot: stub.before ctx.device(mobile)
4
+ [16:29:47.576] waygraph pilot: stub.before ctx.zoom(1.35)
5
+ [16:29:47.576] waygraph pilot: stub.before ring #user-name tone=info label="Username"
6
+ [16:29:48.942] waygraph pilot: stub.after ctx.todos n=6 index=1
7
+ [16:29:50.794] waygraph pilot: stub.before ring #password tone=planned label="Password"
8
+ [16:29:50.965] waygraph pilot: stub.after ctx.todos n=6 index=2
9
+ [16:29:52.406] waygraph pilot: stub.before ring #login-button tone=warning label="Login"
10
+ [16:29:52.726] waygraph pilot: stub.after ctx.title("Inventory · landscape")
11
+ [16:29:52.727] waygraph pilot: stub.after ctx.todos n=6 index=3
12
+ [16:29:52.727] waygraph pilot: stub.after ctx.device(mobile)
13
+ [16:29:52.727] waygraph pilot: stub.after ring .inventory_list tone=success label="Product shelf"
14
+ [16:29:54.148] waygraph pilot: ring not painted - selector matched nothing (or a zero-size element): .inventory_list
15
+ [16:29:58.462] waygraph pilot: stub.before ctx.title("Open cart · landscape")
16
+ [16:29:58.462] waygraph pilot: stub.before ctx.todos n=6 index=5
17
+ [16:29:58.462] waygraph pilot: stub.before ctx.device(mobile)
18
+ [16:29:58.462] waygraph pilot: stub.before ctx.zoom(1.4)
19
+ [16:29:58.462] waygraph pilot: stub.before ring .shopping_cart_link tone=planned label="Cart"
20
+ [16:29:58.870] waygraph pilot: stub.after ctx.title("Your cart · desktop")
21
+ [16:29:58.871] waygraph pilot: stub.after ring .cart_item tone=success label="Cart items"
22
+ [16:29:58.934] waygraph pilot: ring not painted - selector matched nothing (or a zero-size element): .cart_item
@@ -0,0 +1,4 @@
1
+ [16:29:18.968] waygraph pilot: highlight ring #user-name tone=planned label="Username"
2
+ [16:29:18.969] waygraph pilot: highlight ring #password tone=info label="Password"
3
+ [16:29:18.969] waygraph pilot: highlight ring #login-button tone=success label="Login"
4
+ [16:29:23.306] waygraph pilot: highlight ring #user-name tone=planned label="Username"
File without changes
@@ -0,0 +1,22 @@
1
+ [16:29:27.265] waygraph pilot: stub.before ctx.title("Signing in · mobile")
2
+ [16:29:27.265] waygraph pilot: stub.before ctx.todos n=6 index=0
3
+ [16:29:27.265] waygraph pilot: stub.before ctx.device(mobile)
4
+ [16:29:27.266] waygraph pilot: stub.before ctx.zoom(1.35)
5
+ [16:29:27.266] waygraph pilot: stub.before ring #user-name tone=info label="Username"
6
+ [16:29:28.809] waygraph pilot: stub.after ctx.todos n=6 index=1
7
+ [16:29:34.946] waygraph pilot: stub.before ring #password tone=planned label="Password"
8
+ [16:29:35.328] waygraph pilot: stub.after ctx.todos n=6 index=2
9
+ [16:29:41.327] waygraph pilot: stub.before ring #login-button tone=warning label="Login"
10
+ [16:29:42.199] waygraph pilot: stub.after ctx.title("Inventory · landscape")
11
+ [16:29:42.200] waygraph pilot: stub.after ctx.todos n=6 index=3
12
+ [16:29:42.200] waygraph pilot: stub.after ctx.device(mobile)
13
+ [16:29:42.200] waygraph pilot: stub.after ring .inventory_list tone=success label="Product shelf"
14
+ [16:29:50.154] waygraph pilot: stub.before ctx.title("Pick a product · tablet")
15
+ [16:29:50.154] waygraph pilot: stub.before ctx.todos n=6 index=3
16
+ [16:29:50.154] waygraph pilot: stub.before ctx.device(tablet)
17
+ [16:29:50.154] waygraph pilot: stub.before ctx.zoom(1.35)
18
+ [16:29:50.154] waygraph pilot: stub.before ring div.inventory_item:has(button[data-test="add-to-cart-sauce-labs-backpack"], button[data-test="remove-sauce-labs-backpack"]) .inventory_item_name tone=info label="Sauce Labs Backpack"
19
+ [16:29:50.154] waygraph pilot: stub.before ring [data-test="add-to-cart-sauce-labs-backpack"] tone=success label="Add to cart"
20
+ [16:29:51.029] waygraph pilot: stub.after ctx.title("In the cart")
21
+ [16:29:51.029] waygraph pilot: stub.after ctx.todos n=6 index=4
22
+ [16:29:51.029] waygraph pilot: stub.after ring .shopping_cart_badge tone=success label="Cart badge"
@@ -0,0 +1,6 @@
1
+ [16:30:31.665] waygraph pilot: stub.before ctx.title("Signing in · mobile")
2
+ [16:30:31.665] waygraph pilot: stub.before ctx.todos n=6 index=0
3
+ [16:30:31.665] waygraph pilot: stub.before ctx.device(mobile)
4
+ [16:30:31.665] waygraph pilot: stub.before ctx.zoom(1.35)
5
+ [16:30:31.665] waygraph pilot: stub.before ring #user-name tone=info label="Username"
6
+ [16:30:32.427] waygraph pilot: stub.after ctx.todos n=6 index=1
File without changes
@@ -0,0 +1,6 @@
1
+ [16:30:12.884] waygraph pilot: highlight ctx.todos n=3 index=0
2
+ [16:30:12.884] waygraph pilot: highlight ring #user-name tone=planned label="Username"
3
+ [16:30:12.892] waygraph pilot: highlight ring #password tone=info label="Password"
4
+ [16:30:17.796] waygraph pilot: highlight ring #no-such-element-waygraph tone=danger label="Gone"
5
+ [16:30:17.817] waygraph pilot: ring not painted - selector matched nothing (or a zero-size element): #no-such-element-waygraph
6
+ [16:30:20.108] waygraph pilot: highlight ring #login-button tone=success label="Login"
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waygraph",
3
- "version": "0.15.49",
3
+ "version": "0.15.50",
4
4
  "type": "module",
5
5
  "description": "A typed graph of reusable Blocks for driving a browser through E2E flows.",
6
6
  "license": "MIT",
@@ -5,21 +5,34 @@ import {
5
5
  withTitle,
6
6
  withHighlightFixtures,
7
7
  withSessionReset,
8
+ withMemStub,
9
+ registerMemStub,
8
10
  } from "waygraph";
9
11
  import { NavHomeBlock } from "../map/(app_base)/home/_nav.block.js";
10
12
  import { AddItemBlock } from "../map/(app_base)/home/_methods/add-item.effect.block.js";
11
13
  import { AssertItemAddedBlock } from "../map/(app_base)/home/_methods/assert-item-added.method.block.js";
12
14
  import { RemoveItemBlock } from "../map/(app_base)/home/_methods/remove-item.effect.block.js";
13
15
  import { ClearCartBlock } from "../map/(app_base)/home/_methods/clear-cart.method.block.js";
16
+ import { SelectedItem } from "../states/demo.mem-keys.js";
14
17
  import "../map/(app_base)/home/_page.block.js";
15
18
 
16
19
  const engine = new Engine();
17
20
 
18
21
  /**
19
- * Effect + Method demo (set mem for add-item, or use `waygraph auto` which
20
- * fills SelectedItem via instanceOptions).
22
+ * memStub: a real fixture row (`alpha`/`Alpha`), registered once so this Flow never needs
23
+ * --data at all. Still override-able - `--data '{"demo.selectedItem":{"id":"beta",...}}'`
24
+ * wins over this registration, same as any other memStub (see docs/REFERENCE.md's own
25
+ * "memStub - fake values for requires" section).
26
+ */
27
+ registerMemStub(SelectedItem.key, () => ({ id: "alpha", name: "Alpha" }));
28
+
29
+ /**
30
+ * Effect + Method demo. `requires: [SelectedItem.key]` (on `add-item`) is filled automatically
31
+ * by the memStub registration above - no --data needed. Override it with real data, or use
32
+ * `waygraph auto` which fills SelectedItem via instanceOptions instead:
21
33
  *
22
- * waygraph demo --blocks shopFlow --data '{"demo.selectedItem":{"id":"alpha","name":"Alpha"}}'
34
+ * waygraph demo --blocks shopFlow # memStub fills it
35
+ * waygraph demo --blocks shopFlow --data '{"demo.selectedItem":{"id":"beta","name":"Beta"}}'
23
36
  * waygraph auto # after landing on home, pick Add Alpha / Add Beta / Remove ...
24
37
  *
25
38
  * Add -> assert it actually landed (mem-matched Remove button + cart count,
@@ -28,32 +41,34 @@ const engine = new Engine();
28
41
  * Clear cart (bulk Method, `clear-cart`) - keeps both the per-item and bulk
29
42
  * clear paths atomic and each wired into a real flow, not orphaned.
30
43
  */
31
- export const shopFlow = withHighlightFixtures(
32
- withTitle(
33
- withSessionReset(
34
- engine.defineFlow([
35
- start,
36
- NavHomeBlock,
37
- AddItemBlock,
38
- AssertItemAddedBlock,
39
- RemoveItemBlock,
40
- AddItemBlock,
41
- ClearCartBlock,
42
- end,
43
- ]),
44
+ export const shopFlow = withMemStub(
45
+ withHighlightFixtures(
46
+ withTitle(
47
+ withSessionReset(
48
+ engine.defineFlow([
49
+ start,
50
+ NavHomeBlock,
51
+ AddItemBlock,
52
+ AssertItemAddedBlock,
53
+ RemoveItemBlock,
54
+ AddItemBlock,
55
+ ClearCartBlock,
56
+ end,
57
+ ]),
58
+ ),
59
+ "Scaffold: Shop (Effect + clear)",
44
60
  ),
45
- "Scaffold: Shop (Effect + clear)",
46
- ),
47
- {
48
- "add-item": {
49
- stubAfter: {
50
- cart: {
51
- label: "AC · item in cart",
52
- detail: "Cart count bumped after Add.",
53
- tag: "AC",
54
- duration: true,
61
+ {
62
+ "add-item": {
63
+ stubAfter: {
64
+ cart: {
65
+ label: "AC · item in cart",
66
+ detail: "Cart count bumped after Add.",
67
+ tag: "AC",
68
+ duration: true,
69
+ },
55
70
  },
56
71
  },
57
72
  },
58
- },
73
+ ),
59
74
  );