@step-forge/step-forge 0.0.21 → 0.0.22

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.
Files changed (41) hide show
  1. package/RUNTIME.md +42 -12
  2. package/dist/{analyzer-DYfdoSIS.js → analyzer-byS8yRrY.js} +52 -24
  3. package/dist/analyzer-byS8yRrY.js.map +1 -0
  4. package/dist/analyzer-cli.js +1 -1
  5. package/dist/analyzer.d.ts +2 -0
  6. package/dist/analyzer.js +1 -1
  7. package/dist/cli.cjs +129 -74
  8. package/dist/cli.cjs.map +1 -1
  9. package/dist/cli.js +129 -74
  10. package/dist/cli.js.map +1 -1
  11. package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
  12. package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
  13. package/dist/{engine-DPRxs6eC.js → engine-DPVLEHBi.js} +8 -26
  14. package/dist/engine-DPVLEHBi.js.map +1 -0
  15. package/dist/{engine-DWAIlwWp.cjs → engine-vqA-eL_T.cjs} +8 -26
  16. package/dist/engine-vqA-eL_T.cjs.map +1 -0
  17. package/dist/{gherkinParser-CHpkEwii.cjs → gherkinParser-BT40q_i3.cjs} +97 -2
  18. package/dist/gherkinParser-BT40q_i3.cjs.map +1 -0
  19. package/dist/{gherkinParser-CKARHgt4.js → gherkinParser-NcttZgN4.js} +74 -3
  20. package/dist/gherkinParser-NcttZgN4.js.map +1 -0
  21. package/dist/hooks-BDCMKeNq.js +71 -0
  22. package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
  23. package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
  24. package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
  25. package/dist/runtime.cjs +3 -3
  26. package/dist/runtime.d.cts +11 -48
  27. package/dist/runtime.d.ts +11 -48
  28. package/dist/runtime.js +3 -3
  29. package/dist/step-forge.cjs +65 -28
  30. package/dist/step-forge.cjs.map +1 -1
  31. package/dist/step-forge.d.cts +13 -4
  32. package/dist/step-forge.d.ts +13 -4
  33. package/dist/step-forge.js +65 -28
  34. package/dist/step-forge.js.map +1 -1
  35. package/package.json +2 -2
  36. package/dist/analyzer-DYfdoSIS.js.map +0 -1
  37. package/dist/engine-DPRxs6eC.js.map +0 -1
  38. package/dist/engine-DWAIlwWp.cjs.map +0 -1
  39. package/dist/gherkinParser-CHpkEwii.cjs.map +0 -1
  40. package/dist/gherkinParser-CKARHgt4.js.map +0 -1
  41. package/dist/hooks-CywugMQQ.js +0 -82
@@ -0,0 +1,71 @@
1
+ //#region src/runtime/registry.ts
2
+ /**
3
+ * A collection of registered steps. Deliberately a plain instance (not a hidden
4
+ * module global) so tests and the Vitest plugin can create isolated registries.
5
+ * `globalRegistry` is the default sink that `.step()` writes to.
6
+ */
7
+ var StepRegistry = class {
8
+ steps = [];
9
+ add(step) {
10
+ this.steps.push(step);
11
+ }
12
+ all() {
13
+ return this.steps;
14
+ }
15
+ clear() {
16
+ this.steps = [];
17
+ }
18
+ };
19
+ const globalRegistry = new StepRegistry();
20
+ //#endregion
21
+ //#region src/runtime/hooks.ts
22
+ /**
23
+ * Collection of registered hooks. Like {@link StepRegistry}, a plain instance
24
+ * (not a hidden global) so tests can isolate; `globalHookRegistry` is the
25
+ * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.
26
+ */
27
+ var HookRegistry = class {
28
+ hooks = [];
29
+ add(hook) {
30
+ this.hooks.push(hook);
31
+ }
32
+ /**
33
+ * Hooks for a scope+timing. `before` hooks run in registration order;
34
+ * `after` hooks run in reverse (LIFO), so teardown unwinds setup.
35
+ */
36
+ for(scope, timing) {
37
+ const matching = this.hooks.filter((h) => h.scope === scope && h.timing === timing);
38
+ return timing === "after" ? matching.reverse() : matching;
39
+ }
40
+ clear() {
41
+ this.hooks = [];
42
+ }
43
+ };
44
+ const globalHookRegistry = new HookRegistry();
45
+ /**
46
+ * Run every registered hook of a scope+timing **in registration order** (after
47
+ * hooks reversed by {@link HookRegistry.for}, so teardown unwinds setup). Used
48
+ * for feature hooks, where ordering matters. Throws if a hook throws, so the
49
+ * runner reports it against the enclosing boundary.
50
+ */
51
+ async function runHooks(scope, timing, registry = globalHookRegistry) {
52
+ for (const hook of registry.for(scope, timing)) await hook.fn();
53
+ }
54
+ /**
55
+ * Run every registered hook of a scope+timing **concurrently**, resolving once
56
+ * all of them settle. This is how global `beforeAll`/`afterAll` run: independent
57
+ * setup/teardown steps fire in parallel with no ordering between them. A hook
58
+ * with a sequential requirement should sequence that work inside a single hook.
59
+ *
60
+ * The runner calls this exactly once for `beforeAll` (before any scenario
61
+ * starts) and once for `afterAll` (after every scenario is done), so global
62
+ * setup/teardown brackets the whole run deterministically. Rejects if any hook
63
+ * rejects (via `Promise.all`), surfacing the first failure to the caller.
64
+ */
65
+ async function runHooksParallel(scope, timing, registry = globalHookRegistry) {
66
+ await Promise.all(registry.for(scope, timing).map((hook) => hook.fn()));
67
+ }
68
+ //#endregion
69
+ export { StepRegistry as a, runHooksParallel as i, globalHookRegistry as n, globalRegistry as o, runHooks as r, HookRegistry as t };
70
+
71
+ //# sourceMappingURL=hooks-BDCMKeNq.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-CywugMQQ.js","names":[],"sources":["../../src/runtime/registry.ts","../../src/runtime/hooks.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Parser } from \"../parsers\";\nimport { MergeableWorld } from \"../world\";\n\nexport type StepType = \"given\" | \"when\" | \"then\";\n\n/**\n * A step definition as it exists at *runtime*, independent of any test runner.\n *\n * `execute` is the fully-wired step body: given a world and the raw values\n * captured from a Gherkin step, it applies parsers, validates + narrows\n * dependencies, runs the user's step function, and merges the result back into\n * the world. The engine never needs to know how any of that works — it just\n * matches text to a step and calls `execute`.\n */\nexport interface RegisteredStep {\n stepType: StepType;\n /** The Cucumber-expression source, e.g. `a user named {string}`. */\n expression: string;\n /** Parsers, one per captured variable, applied after expression capture. */\n parsers: Parser<any>[];\n execute: (\n world: MergeableWorld<any, any, any>,\n capturedArgs: unknown[]\n ) => Promise<void>;\n /** Where the step was defined, for ambiguous-match diagnostics. */\n source?: string;\n}\n\n/**\n * A collection of registered steps. Deliberately a plain instance (not a hidden\n * module global) so tests and the Vitest plugin can create isolated registries.\n * `globalRegistry` is the default sink that `.step()` writes to.\n */\nexport class StepRegistry {\n private steps: RegisteredStep[] = [];\n\n add(step: RegisteredStep): void {\n this.steps.push(step);\n }\n\n all(): readonly RegisteredStep[] {\n return this.steps;\n }\n\n clear(): void {\n this.steps = [];\n }\n}\n\nexport const globalRegistry = new StepRegistry();\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MergeableWorld } from \"../world\";\n\n/**\n * Hooks are side-effect callbacks that run around scenarios, feature files, or\n * the whole run. Unlike steps they never seed state: state is owned end to end\n * by the typed dependency graph (given/when/then), and hooks exist for setup and\n * teardown (opening a DB, starting a server, resetting mocks). A scenario hook\n * may *read* the world, but its return value is ignored.\n */\nexport type HookScope = \"scenario\" | \"feature\" | \"global\";\nexport type HookTiming = \"before\" | \"after\";\n\n/** Lightweight scenario identity handed to per-scenario hooks. */\nexport interface ScenarioInfo {\n name: string;\n file: string;\n}\n\n/** A per-scenario hook: gets the scenario's world (read-only in spirit). */\nexport type ScenarioHookFn = (context: {\n world: MergeableWorld<any, any, any>;\n scenario: ScenarioInfo;\n}) => void | Promise<void>;\n\n/** A per-feature-file or global hook: no world exists at these boundaries. */\nexport type PlainHookFn = () => void | Promise<void>;\n\nexport interface RegisteredHook {\n scope: HookScope;\n timing: HookTiming;\n fn: ScenarioHookFn | PlainHookFn;\n}\n\n/**\n * Collection of registered hooks. Like {@link StepRegistry}, a plain instance\n * (not a hidden global) so tests can isolate; `globalHookRegistry` is the\n * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.\n */\nexport class HookRegistry {\n private hooks: RegisteredHook[] = [];\n\n add(hook: RegisteredHook): void {\n this.hooks.push(hook);\n }\n\n /**\n * Hooks for a scope+timing. `before` hooks run in registration order;\n * `after` hooks run in reverse (LIFO), so teardown unwinds setup.\n */\n for(scope: HookScope, timing: HookTiming): RegisteredHook[] {\n const matching = this.hooks.filter(\n h => h.scope === scope && h.timing === timing\n );\n return timing === \"after\" ? matching.reverse() : matching;\n }\n\n clear(): void {\n this.hooks = [];\n }\n}\n\nexport const globalHookRegistry = new HookRegistry();\n\n/**\n * Run every registered feature/global hook of a scope+timing in order. Used by\n * the generated test modules (feature hooks). Throws if a hook throws, so the\n * runner reports it against the enclosing boundary.\n */\nexport async function runHooks(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n for (const hook of registry.for(scope, timing)) {\n await (hook.fn as PlainHookFn)();\n }\n}\n\n// Process-global (via Symbol.for so it survives module duplication) guard so\n// global hooks fire exactly once per worker, no matter how many feature modules\n// call in.\nconst GLOBAL_GUARD = Symbol.for(\"step-forge.globalHooksStarted\");\n\n/**\n * Run global before-hooks once per worker, ahead of that worker's first\n * scenario, and schedule global after-hooks for worker exit. Idempotent: every\n * feature module calls this in a `beforeAll`, but only the first call in a given\n * worker does anything.\n *\n * Semantics & caveats (the once-per-worker model):\n * - Runs in the *same* realm as steps, so global setup may touch in-process\n * state that steps later read.\n * - \"Once per worker\", not strictly once per run — with multiple workers it runs\n * in each. Size global setup to be worker-safe (e.g. a server per worker).\n * - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and\n * are not awaited by the runner, so keep them fast/synchronous.\n */\nexport async function ensureGlobalHooks(\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n const store = globalThis as Record<symbol, boolean>;\n if (store[GLOBAL_GUARD]) return;\n store[GLOBAL_GUARD] = true;\n\n for (const hook of registry.for(\"global\", \"before\")) {\n await (hook.fn as PlainHookFn)();\n }\n\n process.once(\"beforeExit\", () => {\n void (async () => {\n for (const hook of registry.for(\"global\", \"after\")) {\n await (hook.fn as PlainHookFn)();\n }\n })();\n });\n}\n"],"mappings":";;;;;;AAkCA,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;CAEA,MAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,iBAAiB,IAAI,aAAa;;;;;;;;ACX/C,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;;;;;CAMA,IAAI,OAAkB,QAAsC;EAC1D,MAAM,WAAW,KAAK,MAAM,QAC1B,MAAK,EAAE,UAAU,SAAS,EAAE,WAAW,MACzC;EACA,OAAO,WAAW,UAAU,SAAS,QAAQ,IAAI;CACnD;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,qBAAqB,IAAI,aAAa;;;;;;AAOnD,eAAsB,SACpB,OACA,QACA,WAAyB,oBACV;CACf,KAAK,MAAM,QAAQ,SAAS,IAAI,OAAO,MAAM,GAC3C,MAAO,KAAK,GAAmB;AAEnC;AAKA,MAAM,eAAe,OAAO,IAAI,+BAA+B;;;;;;;;;;;;;;;AAgB/D,eAAsB,kBACpB,WAAyB,oBACV;CACf,MAAM,QAAQ;CACd,IAAI,MAAM,eAAe;CACzB,MAAM,gBAAgB;CAEtB,KAAK,MAAM,QAAQ,SAAS,IAAI,UAAU,QAAQ,GAChD,MAAO,KAAK,GAAmB;CAGjC,QAAQ,KAAK,oBAAoB;EAC/B,CAAM,YAAY;GAChB,KAAK,MAAM,QAAQ,SAAS,IAAI,UAAU,OAAO,GAC/C,MAAO,KAAK,GAAmB;EAEnC,EAAA,CAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"hooks-BDCMKeNq.js","names":[],"sources":["../../src/runtime/registry.ts","../../src/runtime/hooks.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Parser } from \"../parsers\";\nimport { MergeableWorld } from \"../world\";\n\nexport type StepType = \"given\" | \"when\" | \"then\";\n\n/**\n * A step definition as it exists at *runtime*, independent of any test runner.\n *\n * `execute` is the fully-wired step body: given a world and the raw values\n * captured from a Gherkin step, it applies parsers, validates + narrows\n * dependencies, runs the user's step function, and merges the result back into\n * the world. The engine never needs to know how any of that works — it just\n * matches text to a step and calls `execute`.\n */\nexport interface RegisteredStep {\n stepType: StepType;\n /** The Cucumber-expression source, e.g. `a user named {string}`. */\n expression: string;\n /** Parsers, one per captured variable, applied after expression capture. */\n parsers: Parser<any>[];\n execute: (\n world: MergeableWorld<any, any, any>,\n capturedArgs: unknown[]\n ) => Promise<void>;\n /** Where the step was defined, for ambiguous-match diagnostics. */\n source?: string;\n}\n\n/**\n * A collection of registered steps. Deliberately a plain instance (not a hidden\n * module global) so tests and the Vitest plugin can create isolated registries.\n * `globalRegistry` is the default sink that `.step()` writes to.\n */\nexport class StepRegistry {\n private steps: RegisteredStep[] = [];\n\n add(step: RegisteredStep): void {\n this.steps.push(step);\n }\n\n all(): readonly RegisteredStep[] {\n return this.steps;\n }\n\n clear(): void {\n this.steps = [];\n }\n}\n\nexport const globalRegistry = new StepRegistry();\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MergeableWorld } from \"../world\";\n\n/**\n * Hooks are side-effect callbacks that run around scenarios, feature files, or\n * the whole run. Unlike steps they never seed state: state is owned end to end\n * by the typed dependency graph (given/when/then), and hooks exist for setup and\n * teardown (opening a DB, starting a server, resetting mocks). A scenario hook\n * may *read* the world, but its return value is ignored.\n */\nexport type HookScope = \"scenario\" | \"feature\" | \"global\";\nexport type HookTiming = \"before\" | \"after\";\n\n/** Lightweight scenario identity handed to per-scenario hooks. */\nexport interface ScenarioInfo {\n name: string;\n file: string;\n}\n\n/** A per-scenario hook: gets the scenario's world (read-only in spirit). */\nexport type ScenarioHookFn = (context: {\n world: MergeableWorld<any, any, any>;\n scenario: ScenarioInfo;\n}) => void | Promise<void>;\n\n/** A per-feature-file or global hook: no world exists at these boundaries. */\nexport type PlainHookFn = () => void | Promise<void>;\n\nexport interface RegisteredHook {\n scope: HookScope;\n timing: HookTiming;\n fn: ScenarioHookFn | PlainHookFn;\n}\n\n/**\n * Collection of registered hooks. Like {@link StepRegistry}, a plain instance\n * (not a hidden global) so tests can isolate; `globalHookRegistry` is the\n * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.\n */\nexport class HookRegistry {\n private hooks: RegisteredHook[] = [];\n\n add(hook: RegisteredHook): void {\n this.hooks.push(hook);\n }\n\n /**\n * Hooks for a scope+timing. `before` hooks run in registration order;\n * `after` hooks run in reverse (LIFO), so teardown unwinds setup.\n */\n for(scope: HookScope, timing: HookTiming): RegisteredHook[] {\n const matching = this.hooks.filter(\n h => h.scope === scope && h.timing === timing\n );\n return timing === \"after\" ? matching.reverse() : matching;\n }\n\n clear(): void {\n this.hooks = [];\n }\n}\n\nexport const globalHookRegistry = new HookRegistry();\n\n/**\n * Run every registered hook of a scope+timing **in registration order** (after\n * hooks reversed by {@link HookRegistry.for}, so teardown unwinds setup). Used\n * for feature hooks, where ordering matters. Throws if a hook throws, so the\n * runner reports it against the enclosing boundary.\n */\nexport async function runHooks(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n for (const hook of registry.for(scope, timing)) {\n await (hook.fn as PlainHookFn)();\n }\n}\n\n/**\n * Run every registered hook of a scope+timing **concurrently**, resolving once\n * all of them settle. This is how global `beforeAll`/`afterAll` run: independent\n * setup/teardown steps fire in parallel with no ordering between them. A hook\n * with a sequential requirement should sequence that work inside a single hook.\n *\n * The runner calls this exactly once for `beforeAll` (before any scenario\n * starts) and once for `afterAll` (after every scenario is done), so global\n * setup/teardown brackets the whole run deterministically. Rejects if any hook\n * rejects (via `Promise.all`), surfacing the first failure to the caller.\n */\nexport async function runHooksParallel(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n await Promise.all(\n registry.for(scope, timing).map(hook => (hook.fn as PlainHookFn)())\n );\n}\n"],"mappings":";;;;;;AAkCA,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;CAEA,MAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,iBAAiB,IAAI,aAAa;;;;;;;;ACX/C,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;;;;;CAMA,IAAI,OAAkB,QAAsC;EAC1D,MAAM,WAAW,KAAK,MAAM,QAC1B,MAAK,EAAE,UAAU,SAAS,EAAE,WAAW,MACzC;EACA,OAAO,WAAW,UAAU,SAAS,QAAQ,IAAI;CACnD;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,qBAAqB,IAAI,aAAa;;;;;;;AAQnD,eAAsB,SACpB,OACA,QACA,WAAyB,oBACV;CACf,KAAK,MAAM,QAAQ,SAAS,IAAI,OAAO,MAAM,GAC3C,MAAO,KAAK,GAAmB;AAEnC;;;;;;;;;;;;AAaA,eAAsB,iBACpB,OACA,QACA,WAAyB,oBACV;CACf,MAAM,QAAQ,IACZ,SAAS,IAAI,OAAO,MAAM,CAAC,CAAC,KAAI,SAAS,KAAK,GAAmB,CAAC,CACpE;AACF"}
@@ -43,38 +43,27 @@ var HookRegistry = class {
43
43
  };
44
44
  const globalHookRegistry = new HookRegistry();
45
45
  /**
46
- * Run every registered feature/global hook of a scope+timing in order. Used by
47
- * the generated test modules (feature hooks). Throws if a hook throws, so the
46
+ * Run every registered hook of a scope+timing **in registration order** (after
47
+ * hooks reversed by {@link HookRegistry.for}, so teardown unwinds setup). Used
48
+ * for feature hooks, where ordering matters. Throws if a hook throws, so the
48
49
  * runner reports it against the enclosing boundary.
49
50
  */
50
51
  async function runHooks(scope, timing, registry = globalHookRegistry) {
51
52
  for (const hook of registry.for(scope, timing)) await hook.fn();
52
53
  }
53
- const GLOBAL_GUARD = Symbol.for("step-forge.globalHooksStarted");
54
54
  /**
55
- * Run global before-hooks once per worker, ahead of that worker's first
56
- * scenario, and schedule global after-hooks for worker exit. Idempotent: every
57
- * feature module calls this in a `beforeAll`, but only the first call in a given
58
- * worker does anything.
55
+ * Run every registered hook of a scope+timing **concurrently**, resolving once
56
+ * all of them settle. This is how global `beforeAll`/`afterAll` run: independent
57
+ * setup/teardown steps fire in parallel with no ordering between them. A hook
58
+ * with a sequential requirement should sequence that work inside a single hook.
59
59
  *
60
- * Semantics & caveats (the once-per-worker model):
61
- * - Runs in the *same* realm as steps, so global setup may touch in-process
62
- * state that steps later read.
63
- * - "Once per worker", not strictly once per run with multiple workers it runs
64
- * in each. Size global setup to be worker-safe (e.g. a server per worker).
65
- * - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and
66
- * are not awaited by the runner, so keep them fast/synchronous.
60
+ * The runner calls this exactly once for `beforeAll` (before any scenario
61
+ * starts) and once for `afterAll` (after every scenario is done), so global
62
+ * setup/teardown brackets the whole run deterministically. Rejects if any hook
63
+ * rejects (via `Promise.all`), surfacing the first failure to the caller.
67
64
  */
68
- async function ensureGlobalHooks(registry = globalHookRegistry) {
69
- const store = globalThis;
70
- if (store[GLOBAL_GUARD]) return;
71
- store[GLOBAL_GUARD] = true;
72
- for (const hook of registry.for("global", "before")) await hook.fn();
73
- process.once("beforeExit", () => {
74
- (async () => {
75
- for (const hook of registry.for("global", "after")) await hook.fn();
76
- })();
77
- });
65
+ async function runHooksParallel(scope, timing, registry = globalHookRegistry) {
66
+ await Promise.all(registry.for(scope, timing).map((hook) => hook.fn()));
78
67
  }
79
68
  //#endregion
80
69
  Object.defineProperty(exports, "HookRegistry", {
@@ -89,12 +78,6 @@ Object.defineProperty(exports, "StepRegistry", {
89
78
  return StepRegistry;
90
79
  }
91
80
  });
92
- Object.defineProperty(exports, "ensureGlobalHooks", {
93
- enumerable: true,
94
- get: function() {
95
- return ensureGlobalHooks;
96
- }
97
- });
98
81
  Object.defineProperty(exports, "globalHookRegistry", {
99
82
  enumerable: true,
100
83
  get: function() {
@@ -113,5 +96,11 @@ Object.defineProperty(exports, "runHooks", {
113
96
  return runHooks;
114
97
  }
115
98
  });
99
+ Object.defineProperty(exports, "runHooksParallel", {
100
+ enumerable: true,
101
+ get: function() {
102
+ return runHooksParallel;
103
+ }
104
+ });
116
105
 
117
- //# sourceMappingURL=hooks-CGYzwDOv.cjs.map
106
+ //# sourceMappingURL=hooks-Be0cjULN.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"hooks-CGYzwDOv.cjs","names":[],"sources":["../../src/runtime/registry.ts","../../src/runtime/hooks.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Parser } from \"../parsers\";\nimport { MergeableWorld } from \"../world\";\n\nexport type StepType = \"given\" | \"when\" | \"then\";\n\n/**\n * A step definition as it exists at *runtime*, independent of any test runner.\n *\n * `execute` is the fully-wired step body: given a world and the raw values\n * captured from a Gherkin step, it applies parsers, validates + narrows\n * dependencies, runs the user's step function, and merges the result back into\n * the world. The engine never needs to know how any of that works — it just\n * matches text to a step and calls `execute`.\n */\nexport interface RegisteredStep {\n stepType: StepType;\n /** The Cucumber-expression source, e.g. `a user named {string}`. */\n expression: string;\n /** Parsers, one per captured variable, applied after expression capture. */\n parsers: Parser<any>[];\n execute: (\n world: MergeableWorld<any, any, any>,\n capturedArgs: unknown[]\n ) => Promise<void>;\n /** Where the step was defined, for ambiguous-match diagnostics. */\n source?: string;\n}\n\n/**\n * A collection of registered steps. Deliberately a plain instance (not a hidden\n * module global) so tests and the Vitest plugin can create isolated registries.\n * `globalRegistry` is the default sink that `.step()` writes to.\n */\nexport class StepRegistry {\n private steps: RegisteredStep[] = [];\n\n add(step: RegisteredStep): void {\n this.steps.push(step);\n }\n\n all(): readonly RegisteredStep[] {\n return this.steps;\n }\n\n clear(): void {\n this.steps = [];\n }\n}\n\nexport const globalRegistry = new StepRegistry();\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MergeableWorld } from \"../world\";\n\n/**\n * Hooks are side-effect callbacks that run around scenarios, feature files, or\n * the whole run. Unlike steps they never seed state: state is owned end to end\n * by the typed dependency graph (given/when/then), and hooks exist for setup and\n * teardown (opening a DB, starting a server, resetting mocks). A scenario hook\n * may *read* the world, but its return value is ignored.\n */\nexport type HookScope = \"scenario\" | \"feature\" | \"global\";\nexport type HookTiming = \"before\" | \"after\";\n\n/** Lightweight scenario identity handed to per-scenario hooks. */\nexport interface ScenarioInfo {\n name: string;\n file: string;\n}\n\n/** A per-scenario hook: gets the scenario's world (read-only in spirit). */\nexport type ScenarioHookFn = (context: {\n world: MergeableWorld<any, any, any>;\n scenario: ScenarioInfo;\n}) => void | Promise<void>;\n\n/** A per-feature-file or global hook: no world exists at these boundaries. */\nexport type PlainHookFn = () => void | Promise<void>;\n\nexport interface RegisteredHook {\n scope: HookScope;\n timing: HookTiming;\n fn: ScenarioHookFn | PlainHookFn;\n}\n\n/**\n * Collection of registered hooks. Like {@link StepRegistry}, a plain instance\n * (not a hidden global) so tests can isolate; `globalHookRegistry` is the\n * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.\n */\nexport class HookRegistry {\n private hooks: RegisteredHook[] = [];\n\n add(hook: RegisteredHook): void {\n this.hooks.push(hook);\n }\n\n /**\n * Hooks for a scope+timing. `before` hooks run in registration order;\n * `after` hooks run in reverse (LIFO), so teardown unwinds setup.\n */\n for(scope: HookScope, timing: HookTiming): RegisteredHook[] {\n const matching = this.hooks.filter(\n h => h.scope === scope && h.timing === timing\n );\n return timing === \"after\" ? matching.reverse() : matching;\n }\n\n clear(): void {\n this.hooks = [];\n }\n}\n\nexport const globalHookRegistry = new HookRegistry();\n\n/**\n * Run every registered feature/global hook of a scope+timing in order. Used by\n * the generated test modules (feature hooks). Throws if a hook throws, so the\n * runner reports it against the enclosing boundary.\n */\nexport async function runHooks(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n for (const hook of registry.for(scope, timing)) {\n await (hook.fn as PlainHookFn)();\n }\n}\n\n// Process-global (via Symbol.for so it survives module duplication) guard so\n// global hooks fire exactly once per worker, no matter how many feature modules\n// call in.\nconst GLOBAL_GUARD = Symbol.for(\"step-forge.globalHooksStarted\");\n\n/**\n * Run global before-hooks once per worker, ahead of that worker's first\n * scenario, and schedule global after-hooks for worker exit. Idempotent: every\n * feature module calls this in a `beforeAll`, but only the first call in a given\n * worker does anything.\n *\n * Semantics & caveats (the once-per-worker model):\n * - Runs in the *same* realm as steps, so global setup may touch in-process\n * state that steps later read.\n * - \"Once per worker\", not strictly once per run — with multiple workers it runs\n * in each. Size global setup to be worker-safe (e.g. a server per worker).\n * - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and\n * are not awaited by the runner, so keep them fast/synchronous.\n */\nexport async function ensureGlobalHooks(\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n const store = globalThis as Record<symbol, boolean>;\n if (store[GLOBAL_GUARD]) return;\n store[GLOBAL_GUARD] = true;\n\n for (const hook of registry.for(\"global\", \"before\")) {\n await (hook.fn as PlainHookFn)();\n }\n\n process.once(\"beforeExit\", () => {\n void (async () => {\n for (const hook of registry.for(\"global\", \"after\")) {\n await (hook.fn as PlainHookFn)();\n }\n })();\n });\n}\n"],"mappings":";;;;;;AAkCA,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;CAEA,MAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,iBAAiB,IAAI,aAAa;;;;;;;;ACX/C,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;;;;;CAMA,IAAI,OAAkB,QAAsC;EAC1D,MAAM,WAAW,KAAK,MAAM,QAC1B,MAAK,EAAE,UAAU,SAAS,EAAE,WAAW,MACzC;EACA,OAAO,WAAW,UAAU,SAAS,QAAQ,IAAI;CACnD;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,qBAAqB,IAAI,aAAa;;;;;;AAOnD,eAAsB,SACpB,OACA,QACA,WAAyB,oBACV;CACf,KAAK,MAAM,QAAQ,SAAS,IAAI,OAAO,MAAM,GAC3C,MAAO,KAAK,GAAmB;AAEnC;AAKA,MAAM,eAAe,OAAO,IAAI,+BAA+B;;;;;;;;;;;;;;;AAgB/D,eAAsB,kBACpB,WAAyB,oBACV;CACf,MAAM,QAAQ;CACd,IAAI,MAAM,eAAe;CACzB,MAAM,gBAAgB;CAEtB,KAAK,MAAM,QAAQ,SAAS,IAAI,UAAU,QAAQ,GAChD,MAAO,KAAK,GAAmB;CAGjC,QAAQ,KAAK,oBAAoB;EAC/B,CAAM,YAAY;GAChB,KAAK,MAAM,QAAQ,SAAS,IAAI,UAAU,OAAO,GAC/C,MAAO,KAAK,GAAmB;EAEnC,EAAA,CAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"hooks-Be0cjULN.cjs","names":[],"sources":["../../src/runtime/registry.ts","../../src/runtime/hooks.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { Parser } from \"../parsers\";\nimport { MergeableWorld } from \"../world\";\n\nexport type StepType = \"given\" | \"when\" | \"then\";\n\n/**\n * A step definition as it exists at *runtime*, independent of any test runner.\n *\n * `execute` is the fully-wired step body: given a world and the raw values\n * captured from a Gherkin step, it applies parsers, validates + narrows\n * dependencies, runs the user's step function, and merges the result back into\n * the world. The engine never needs to know how any of that works — it just\n * matches text to a step and calls `execute`.\n */\nexport interface RegisteredStep {\n stepType: StepType;\n /** The Cucumber-expression source, e.g. `a user named {string}`. */\n expression: string;\n /** Parsers, one per captured variable, applied after expression capture. */\n parsers: Parser<any>[];\n execute: (\n world: MergeableWorld<any, any, any>,\n capturedArgs: unknown[]\n ) => Promise<void>;\n /** Where the step was defined, for ambiguous-match diagnostics. */\n source?: string;\n}\n\n/**\n * A collection of registered steps. Deliberately a plain instance (not a hidden\n * module global) so tests and the Vitest plugin can create isolated registries.\n * `globalRegistry` is the default sink that `.step()` writes to.\n */\nexport class StepRegistry {\n private steps: RegisteredStep[] = [];\n\n add(step: RegisteredStep): void {\n this.steps.push(step);\n }\n\n all(): readonly RegisteredStep[] {\n return this.steps;\n }\n\n clear(): void {\n this.steps = [];\n }\n}\n\nexport const globalRegistry = new StepRegistry();\n","/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { MergeableWorld } from \"../world\";\n\n/**\n * Hooks are side-effect callbacks that run around scenarios, feature files, or\n * the whole run. Unlike steps they never seed state: state is owned end to end\n * by the typed dependency graph (given/when/then), and hooks exist for setup and\n * teardown (opening a DB, starting a server, resetting mocks). A scenario hook\n * may *read* the world, but its return value is ignored.\n */\nexport type HookScope = \"scenario\" | \"feature\" | \"global\";\nexport type HookTiming = \"before\" | \"after\";\n\n/** Lightweight scenario identity handed to per-scenario hooks. */\nexport interface ScenarioInfo {\n name: string;\n file: string;\n}\n\n/** A per-scenario hook: gets the scenario's world (read-only in spirit). */\nexport type ScenarioHookFn = (context: {\n world: MergeableWorld<any, any, any>;\n scenario: ScenarioInfo;\n}) => void | Promise<void>;\n\n/** A per-feature-file or global hook: no world exists at these boundaries. */\nexport type PlainHookFn = () => void | Promise<void>;\n\nexport interface RegisteredHook {\n scope: HookScope;\n timing: HookTiming;\n fn: ScenarioHookFn | PlainHookFn;\n}\n\n/**\n * Collection of registered hooks. Like {@link StepRegistry}, a plain instance\n * (not a hidden global) so tests can isolate; `globalHookRegistry` is the\n * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.\n */\nexport class HookRegistry {\n private hooks: RegisteredHook[] = [];\n\n add(hook: RegisteredHook): void {\n this.hooks.push(hook);\n }\n\n /**\n * Hooks for a scope+timing. `before` hooks run in registration order;\n * `after` hooks run in reverse (LIFO), so teardown unwinds setup.\n */\n for(scope: HookScope, timing: HookTiming): RegisteredHook[] {\n const matching = this.hooks.filter(\n h => h.scope === scope && h.timing === timing\n );\n return timing === \"after\" ? matching.reverse() : matching;\n }\n\n clear(): void {\n this.hooks = [];\n }\n}\n\nexport const globalHookRegistry = new HookRegistry();\n\n/**\n * Run every registered hook of a scope+timing **in registration order** (after\n * hooks reversed by {@link HookRegistry.for}, so teardown unwinds setup). Used\n * for feature hooks, where ordering matters. Throws if a hook throws, so the\n * runner reports it against the enclosing boundary.\n */\nexport async function runHooks(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n for (const hook of registry.for(scope, timing)) {\n await (hook.fn as PlainHookFn)();\n }\n}\n\n/**\n * Run every registered hook of a scope+timing **concurrently**, resolving once\n * all of them settle. This is how global `beforeAll`/`afterAll` run: independent\n * setup/teardown steps fire in parallel with no ordering between them. A hook\n * with a sequential requirement should sequence that work inside a single hook.\n *\n * The runner calls this exactly once for `beforeAll` (before any scenario\n * starts) and once for `afterAll` (after every scenario is done), so global\n * setup/teardown brackets the whole run deterministically. Rejects if any hook\n * rejects (via `Promise.all`), surfacing the first failure to the caller.\n */\nexport async function runHooksParallel(\n scope: \"feature\" | \"global\",\n timing: HookTiming,\n registry: HookRegistry = globalHookRegistry\n): Promise<void> {\n await Promise.all(\n registry.for(scope, timing).map(hook => (hook.fn as PlainHookFn)())\n );\n}\n"],"mappings":";;;;;;AAkCA,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;CAEA,MAAiC;EAC/B,OAAO,KAAK;CACd;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,iBAAiB,IAAI,aAAa;;;;;;;;ACX/C,IAAa,eAAb,MAA0B;CACxB,QAAkC,CAAC;CAEnC,IAAI,MAA4B;EAC9B,KAAK,MAAM,KAAK,IAAI;CACtB;;;;;CAMA,IAAI,OAAkB,QAAsC;EAC1D,MAAM,WAAW,KAAK,MAAM,QAC1B,MAAK,EAAE,UAAU,SAAS,EAAE,WAAW,MACzC;EACA,OAAO,WAAW,UAAU,SAAS,QAAQ,IAAI;CACnD;CAEA,QAAc;EACZ,KAAK,QAAQ,CAAC;CAChB;AACF;AAEA,MAAa,qBAAqB,IAAI,aAAa;;;;;;;AAQnD,eAAsB,SACpB,OACA,QACA,WAAyB,oBACV;CACf,KAAK,MAAM,QAAQ,SAAS,IAAI,OAAO,MAAM,GAC3C,MAAO,KAAK,GAAmB;AAEnC;;;;;;;;;;;;AAaA,eAAsB,iBACpB,OACA,QACA,WAAyB,oBACV;CACf,MAAM,QAAQ,IACZ,SAAS,IAAI,OAAO,MAAM,CAAC,CAAC,KAAI,SAAS,KAAK,GAAmB,CAAC,CACpE;AACF"}
package/dist/runtime.cjs CHANGED
@@ -1,13 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_hooks = require("./hooks-CGYzwDOv.cjs");
3
- const require_engine = require("./engine-DWAIlwWp.cjs");
2
+ const require_hooks = require("./hooks-Be0cjULN.cjs");
3
+ const require_engine = require("./engine-vqA-eL_T.cjs");
4
4
  exports.AmbiguousStepError = require_engine.AmbiguousStepError;
5
5
  exports.HookRegistry = require_hooks.HookRegistry;
6
6
  exports.StepRegistry = require_hooks.StepRegistry;
7
7
  exports.UndefinedStepError = require_engine.UndefinedStepError;
8
8
  exports.compileRegistry = require_engine.compileRegistry;
9
- exports.ensureGlobalHooks = require_hooks.ensureGlobalHooks;
10
9
  exports.globalHookRegistry = require_hooks.globalHookRegistry;
11
10
  exports.globalRegistry = require_hooks.globalRegistry;
12
11
  exports.runHooks = require_hooks.runHooks;
12
+ exports.runHooksParallel = require_hooks.runHooksParallel;
13
13
  exports.runScenario = require_engine.runScenario;
@@ -1,4 +1,4 @@
1
- import { a as RegisteredHook, b as Parser, c as ensureGlobalHooks, g as ParsedStep, h as ParsedScenario, i as PlainHookFn, l as globalHookRegistry, n as HookScope, o as ScenarioHookFn, r as HookTiming, s as ScenarioInfo, t as HookRegistry, u as runHooks, y as MergeableWorld } from "./hooks-Dar49TtT.cjs";
1
+ import { S as Parser, _ as ParsedScenario, a as HookTiming, c as ScenarioHookFn, d as runHooks, f as runHooksParallel, i as HookScope, l as ScenarioInfo, n as RunnerOptions, o as PlainHookFn, r as HookRegistry, s as RegisteredHook, t as ResolvedConfig, u as globalHookRegistry, v as ParsedStep, x as MergeableWorld } from "./config-C7PCYgYy.cjs";
2
2
  import { CucumberExpression } from "@cucumber/cucumber-expressions";
3
3
 
4
4
  //#region src/runtime/registry.d.ts
@@ -61,6 +61,12 @@ interface StepResult {
61
61
  step: ParsedStep;
62
62
  status: "passed" | "failed" | "skipped";
63
63
  error?: Error;
64
+ /**
65
+ * Absolute `file:line:column` where the matched step is *defined* (its
66
+ * `.step(...)` call site), for Cucumber-style reporting. Absent when no step
67
+ * matched (undefined/ambiguous) or the step was skipped.
68
+ */
69
+ source?: string;
64
70
  durationMs?: number;
65
71
  }
66
72
  interface ScenarioResult {
@@ -69,9 +75,9 @@ interface ScenarioResult {
69
75
  steps: StepResult[];
70
76
  /**
71
77
  * The scenario's first error, if it failed. Usually the same object as the
72
- * failing step's `error` (with a synthetic `.feature` stack frame attached),
73
- * but for a hook failure there's no step to point at, so this is the only
74
- * place it surfaces. Reporters read this; the runner never throws it.
78
+ * failing step's `error`; for a hook failure there's no step to point at, so
79
+ * this is the only place it surfaces. Reporters read this; the runner never
80
+ * throws it.
75
81
  */
76
82
  error?: Error;
77
83
  /** Wall-clock duration of the whole scenario, in milliseconds. */
@@ -95,48 +101,5 @@ interface ScenarioResult {
95
101
  */
96
102
  declare function runScenario(scenario: ParsedScenario, compiled: CompiledStep[], makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
97
103
  //#endregion
98
- //#region src/runtime/config.d.ts
99
- /**
100
- * User-facing runner configuration. The same shape is accepted from a
101
- * `step-forge.config.ts` file (default export) and from CLI flags, with flags
102
- * taking precedence. Everything is optional; {@link resolveConfig} fills in
103
- * defaults.
104
- */
105
- interface RunnerOptions {
106
- /** Feature-file glob(s), relative to `cwd`. Defaults to every `.feature` file. */
107
- features?: string | string[];
108
- /** Step-module glob(s), relative to `cwd`. Defaults to every `.steps.ts` file. */
109
- steps?: string | string[];
110
- /**
111
- * Module that default-exports a world factory `() => world`, relative to
112
- * `cwd`. When omitted each scenario gets a fresh `BasicWorld`.
113
- */
114
- world?: string;
115
- /**
116
- * Max scenarios in flight at once. Defaults to `1` (serial), matching
117
- * Cucumber's default execution model — scenarios often share module-level
118
- * state via hooks, which only holds under serial execution. Raise it to opt
119
- * into parallelism for isolated, I/O-bound suites.
120
- */
121
- concurrency?: number;
122
- /** Reporter name. Default `pretty`. */
123
- reporter?: "pretty" | "progress";
124
- /** Only run scenarios whose name matches this (string → substring/regex). */
125
- name?: string;
126
- /** Cucumber tag expression, e.g. `@smoke and not @wip`. */
127
- tags?: string;
128
- }
129
- /** Fully-resolved config: no optionals, globs kept as arrays, paths absolute. */
130
- interface ResolvedConfig {
131
- cwd: string;
132
- features: string[];
133
- steps: string[];
134
- world?: string;
135
- concurrency: number;
136
- reporter: "pretty" | "progress";
137
- name?: string;
138
- tags?: string;
139
- }
140
- //#endregion
141
- export { AmbiguousStepError, type CompiledStep, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ResolvedConfig, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, compileRegistry, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
104
+ export { AmbiguousStepError, type CompiledStep, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ResolvedConfig, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, compileRegistry, globalHookRegistry, globalRegistry, runHooks, runHooksParallel, runScenario };
142
105
  //# sourceMappingURL=runtime.d.cts.map
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as RegisteredHook, b as Parser, c as ensureGlobalHooks, g as ParsedStep, h as ParsedScenario, i as PlainHookFn, l as globalHookRegistry, n as HookScope, o as ScenarioHookFn, r as HookTiming, s as ScenarioInfo, t as HookRegistry, u as runHooks, y as MergeableWorld } from "./hooks-Dar49TtT.js";
1
+ import { S as Parser, _ as ParsedScenario, a as HookTiming, c as ScenarioHookFn, d as runHooks, f as runHooksParallel, i as HookScope, l as ScenarioInfo, n as RunnerOptions, o as PlainHookFn, r as HookRegistry, s as RegisteredHook, t as ResolvedConfig, u as globalHookRegistry, v as ParsedStep, x as MergeableWorld } from "./config-C7PCYgYy.js";
2
2
  import { CucumberExpression } from "@cucumber/cucumber-expressions";
3
3
 
4
4
  //#region src/runtime/registry.d.ts
@@ -61,6 +61,12 @@ interface StepResult {
61
61
  step: ParsedStep;
62
62
  status: "passed" | "failed" | "skipped";
63
63
  error?: Error;
64
+ /**
65
+ * Absolute `file:line:column` where the matched step is *defined* (its
66
+ * `.step(...)` call site), for Cucumber-style reporting. Absent when no step
67
+ * matched (undefined/ambiguous) or the step was skipped.
68
+ */
69
+ source?: string;
64
70
  durationMs?: number;
65
71
  }
66
72
  interface ScenarioResult {
@@ -69,9 +75,9 @@ interface ScenarioResult {
69
75
  steps: StepResult[];
70
76
  /**
71
77
  * The scenario's first error, if it failed. Usually the same object as the
72
- * failing step's `error` (with a synthetic `.feature` stack frame attached),
73
- * but for a hook failure there's no step to point at, so this is the only
74
- * place it surfaces. Reporters read this; the runner never throws it.
78
+ * failing step's `error`; for a hook failure there's no step to point at, so
79
+ * this is the only place it surfaces. Reporters read this; the runner never
80
+ * throws it.
75
81
  */
76
82
  error?: Error;
77
83
  /** Wall-clock duration of the whole scenario, in milliseconds. */
@@ -95,48 +101,5 @@ interface ScenarioResult {
95
101
  */
96
102
  declare function runScenario(scenario: ParsedScenario, compiled: CompiledStep[], makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
97
103
  //#endregion
98
- //#region src/runtime/config.d.ts
99
- /**
100
- * User-facing runner configuration. The same shape is accepted from a
101
- * `step-forge.config.ts` file (default export) and from CLI flags, with flags
102
- * taking precedence. Everything is optional; {@link resolveConfig} fills in
103
- * defaults.
104
- */
105
- interface RunnerOptions {
106
- /** Feature-file glob(s), relative to `cwd`. Defaults to every `.feature` file. */
107
- features?: string | string[];
108
- /** Step-module glob(s), relative to `cwd`. Defaults to every `.steps.ts` file. */
109
- steps?: string | string[];
110
- /**
111
- * Module that default-exports a world factory `() => world`, relative to
112
- * `cwd`. When omitted each scenario gets a fresh `BasicWorld`.
113
- */
114
- world?: string;
115
- /**
116
- * Max scenarios in flight at once. Defaults to `1` (serial), matching
117
- * Cucumber's default execution model — scenarios often share module-level
118
- * state via hooks, which only holds under serial execution. Raise it to opt
119
- * into parallelism for isolated, I/O-bound suites.
120
- */
121
- concurrency?: number;
122
- /** Reporter name. Default `pretty`. */
123
- reporter?: "pretty" | "progress";
124
- /** Only run scenarios whose name matches this (string → substring/regex). */
125
- name?: string;
126
- /** Cucumber tag expression, e.g. `@smoke and not @wip`. */
127
- tags?: string;
128
- }
129
- /** Fully-resolved config: no optionals, globs kept as arrays, paths absolute. */
130
- interface ResolvedConfig {
131
- cwd: string;
132
- features: string[];
133
- steps: string[];
134
- world?: string;
135
- concurrency: number;
136
- reporter: "pretty" | "progress";
137
- name?: string;
138
- tags?: string;
139
- }
140
- //#endregion
141
- export { AmbiguousStepError, type CompiledStep, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ResolvedConfig, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, compileRegistry, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
104
+ export { AmbiguousStepError, type CompiledStep, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ResolvedConfig, type RunnerOptions, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, compileRegistry, globalHookRegistry, globalRegistry, runHooks, runHooksParallel, runScenario };
142
105
  //# sourceMappingURL=runtime.d.ts.map
package/dist/runtime.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as StepRegistry, i as runHooks, n as ensureGlobalHooks, o as globalRegistry, r as globalHookRegistry, t as HookRegistry } from "./hooks-CywugMQQ.js";
2
- import { i as runScenario, n as UndefinedStepError, r as compileRegistry, t as AmbiguousStepError } from "./engine-DPRxs6eC.js";
3
- export { AmbiguousStepError, HookRegistry, StepRegistry, UndefinedStepError, compileRegistry, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
1
+ import { a as StepRegistry, i as runHooksParallel, n as globalHookRegistry, o as globalRegistry, r as runHooks, t as HookRegistry } from "./hooks-BDCMKeNq.js";
2
+ import { i as runScenario, n as UndefinedStepError, r as compileRegistry, t as AmbiguousStepError } from "./engine-DPVLEHBi.js";
3
+ export { AmbiguousStepError, HookRegistry, StepRegistry, UndefinedStepError, compileRegistry, globalHookRegistry, globalRegistry, runHooks, runHooksParallel, runScenario };
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_gherkinParser = require("./gherkinParser-CHpkEwii.cjs");
3
- const require_hooks = require("./hooks-CGYzwDOv.cjs");
2
+ const require_gherkinParser = require("./gherkinParser-BT40q_i3.cjs");
3
+ const require_hooks = require("./hooks-Be0cjULN.cjs");
4
4
  let lodash = require("lodash");
5
5
  lodash = require_gherkinParser.__toESM(lodash, 1);
6
6
  let typescript = require("typescript");
@@ -117,11 +117,13 @@ const addStep = (statement, stepType, dependencies = {
117
117
  });
118
118
  world[stepType].merge({ ...result });
119
119
  };
120
+ const source = require_gherkinParser.captureDefinitionSite();
120
121
  require_hooks.globalRegistry.add({
121
122
  stepType,
122
123
  expression,
123
124
  parsers,
124
- execute
125
+ execute,
126
+ source
125
127
  });
126
128
  return {
127
129
  statement,
@@ -202,6 +204,21 @@ const BUILDER_NAMES = {
202
204
  thenBuilder: "then"
203
205
  };
204
206
  function extractStepDefinitions(filePaths, tsConfigPath) {
207
+ const fast = extractWithSources(filePaths.map(parseSourceFile).filter((s) => s !== null), null);
208
+ if (!fast.needsChecker) return fast.results;
209
+ const program = typescript.default.createProgram(filePaths, resolveCompilerOptions(tsConfigPath));
210
+ const checker = program.getTypeChecker();
211
+ return extractWithSources(filePaths.map((fp) => program.getSourceFile(fp)).filter((s) => s !== void 0), checker).results;
212
+ }
213
+ /** Parse a single file into an AST with no type resolution. */
214
+ function parseSourceFile(filePath) {
215
+ const text = typescript.default.sys.readFile(filePath);
216
+ if (text === void 0) return null;
217
+ const scriptKind = filePath.endsWith(".tsx") ? typescript.default.ScriptKind.TSX : typescript.default.ScriptKind.TS;
218
+ return typescript.default.createSourceFile(filePath, text, typescript.default.ScriptTarget.ESNext, true, scriptKind);
219
+ }
220
+ /** Resolve compiler options from tsconfig (fallback to sane defaults). */
221
+ function resolveCompilerOptions(tsConfigPath) {
205
222
  const configPath = tsConfigPath ?? typescript.default.findConfigFile(process.cwd(), typescript.default.sys.fileExists);
206
223
  let compilerOptions = {
207
224
  target: typescript.default.ScriptTarget.ESNext,
@@ -215,22 +232,25 @@ function extractStepDefinitions(filePaths, tsConfigPath) {
215
232
  if (!configFile.error) compilerOptions = typescript.default.parseJsonConfigFileContent(configFile.config, typescript.default.sys, configPath.replace(/[/\\][^/\\]+$/, "")).options;
216
233
  }
217
234
  compilerOptions.noEmit = true;
218
- const program = typescript.default.createProgram(filePaths, compilerOptions);
219
- const checker = program.getTypeChecker();
235
+ return compilerOptions;
236
+ }
237
+ function extractWithSources(sources, checker) {
238
+ const ctx = {
239
+ checker,
240
+ needsChecker: false
241
+ };
220
242
  const results = [];
221
- for (const filePath of filePaths) {
222
- const sourceFile = program.getSourceFile(filePath);
223
- if (!sourceFile) continue;
224
- const fileResults = extractFromSourceFile(sourceFile, checker);
225
- results.push(...fileResults);
226
- }
227
- return results;
243
+ for (const sourceFile of sources) results.push(...extractFromSourceFile(sourceFile, ctx));
244
+ return {
245
+ results,
246
+ needsChecker: ctx.needsChecker
247
+ };
228
248
  }
229
- function extractFromSourceFile(sourceFile, checker) {
249
+ function extractFromSourceFile(sourceFile, ctx) {
230
250
  const results = [];
231
251
  function visit(node) {
232
252
  if (typescript.default.isCallExpression(node) && typescript.default.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
233
- const meta = extractFromRegisterCall(node, sourceFile, checker);
253
+ const meta = extractFromRegisterCall(node, sourceFile, ctx);
234
254
  if (meta) results.push(meta);
235
255
  }
236
256
  typescript.default.forEachChild(node, visit);
@@ -238,7 +258,7 @@ function extractFromSourceFile(sourceFile, checker) {
238
258
  visit(sourceFile);
239
259
  return results;
240
260
  }
241
- function extractFromRegisterCall(registerCall, sourceFile, checker) {
261
+ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
242
262
  const chain = collectCallChain(registerCall);
243
263
  let stepType = null;
244
264
  let expression = null;
@@ -253,7 +273,7 @@ function extractFromRegisterCall(registerCall, sourceFile, checker) {
253
273
  if (!name) continue;
254
274
  if (name === "register") continue;
255
275
  if (name === "step") {
256
- produces = extractProducedKeys(link, checker);
276
+ produces = extractProducedKeys(link, ctx);
257
277
  continue;
258
278
  }
259
279
  if (name === "dependencies") {
@@ -270,7 +290,7 @@ function extractFromRegisterCall(registerCall, sourceFile, checker) {
270
290
  }
271
291
  }
272
292
  if (!stepType || !expression) {
273
- const reExport = resolveReExportedCall(chain, checker);
293
+ const reExport = resolveReExportedCall(chain, ctx);
274
294
  if (reExport) {
275
295
  if (!stepType) stepType = reExport.stepType;
276
296
  if (!expression) expression = reExport.expression;
@@ -357,33 +377,37 @@ function extractDependencies(depsCall) {
357
377
  }
358
378
  return deps;
359
379
  }
360
- function extractProducedKeys(stepCall, checker) {
380
+ function extractProducedKeys(stepCall, ctx) {
361
381
  const callback = stepCall.arguments[0];
362
382
  if (!callback) return [];
363
- if (typescript.default.isArrowFunction(callback) || typescript.default.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, checker);
383
+ if (typescript.default.isArrowFunction(callback) || typescript.default.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, ctx);
364
384
  return [];
365
385
  }
366
- function extractProducedKeysFromCallback(callback, checker) {
386
+ function extractProducedKeysFromCallback(callback, ctx) {
367
387
  const body = callback.body;
368
- if (!typescript.default.isBlock(body)) return extractKeysFromExpression(body, checker);
388
+ if (!typescript.default.isBlock(body)) return extractKeysFromExpression(body, ctx);
369
389
  const keys = /* @__PURE__ */ new Set();
370
390
  function visitReturn(node) {
371
- if (typescript.default.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, checker)) keys.add(key);
391
+ if (typescript.default.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, ctx)) keys.add(key);
372
392
  typescript.default.forEachChild(node, visitReturn);
373
393
  }
374
394
  visitReturn(body);
375
395
  return [...keys];
376
396
  }
377
- function extractKeysFromExpression(expr, checker) {
397
+ function extractKeysFromExpression(expr, ctx) {
378
398
  while (typescript.default.isParenthesizedExpression(expr)) expr = expr.expression;
379
399
  if (typescript.default.isObjectLiteralExpression(expr)) return expr.properties.filter((p) => typescript.default.isPropertyAssignment(p) || typescript.default.isShorthandPropertyAssignment(p)).map((p) => p.name.getText()).filter(Boolean);
400
+ if (!ctx.checker) {
401
+ ctx.needsChecker = true;
402
+ return [];
403
+ }
380
404
  try {
381
- return checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
405
+ return ctx.checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
382
406
  } catch {
383
407
  return [];
384
408
  }
385
409
  }
386
- function resolveReExportedCall(chain, checker) {
410
+ function resolveReExportedCall(chain, ctx) {
387
411
  const lastCall = chain[chain.length - 1];
388
412
  if (!lastCall) return null;
389
413
  const expr = lastCall.expression;
@@ -391,7 +415,11 @@ function resolveReExportedCall(chain, checker) {
391
415
  if (typescript.default.isIdentifier(expr)) identifier = expr;
392
416
  else if (typescript.default.isPropertyAccessExpression(expr) && typescript.default.isIdentifier(expr.expression)) identifier = expr.expression;
393
417
  if (!identifier) return null;
394
- const symbol = checker.getSymbolAtLocation(identifier);
418
+ if (!ctx.checker) {
419
+ ctx.needsChecker = true;
420
+ return null;
421
+ }
422
+ const symbol = ctx.checker.getSymbolAtLocation(identifier);
395
423
  if (!symbol) return null;
396
424
  const decl = symbol.valueDeclaration;
397
425
  if (!decl || !typescript.default.isVariableDeclaration(decl) || !decl.initializer) return null;
@@ -611,7 +639,12 @@ function afterFeature(fn) {
611
639
  fn
612
640
  });
613
641
  }
614
- /** Run once before the entire test run, across all feature files. */
642
+ /**
643
+ * Run once before the entire run, ahead of any scenario (and before
644
+ * concurrency starts). Multiple `beforeAll` hooks run **in parallel** with no
645
+ * ordering between them — if a step of setup must precede another, sequence both
646
+ * inside a single hook.
647
+ */
615
648
  function beforeAll(fn) {
616
649
  require_hooks.globalHookRegistry.add({
617
650
  scope: "global",
@@ -619,7 +652,11 @@ function beforeAll(fn) {
619
652
  fn
620
653
  });
621
654
  }
622
- /** Run once after the entire test run (reverse registration order). */
655
+ /**
656
+ * Run once after the entire run, once every scenario is done. Multiple
657
+ * `afterAll` hooks run **in parallel** with no ordering between them — sequence
658
+ * dependent teardown inside a single hook.
659
+ */
623
660
  function afterAll(fn) {
624
661
  require_hooks.globalHookRegistry.add({
625
662
  scope: "global",