@step-forge/step-forge 0.0.20 → 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 (47) hide show
  1. package/README.md +2 -0
  2. package/RUNTIME.md +242 -0
  3. package/dist/{analyzer-DJyJbU_V.js → analyzer-byS8yRrY.js} +202 -34
  4. package/dist/analyzer-byS8yRrY.js.map +1 -0
  5. package/dist/analyzer-cli.js +1 -1
  6. package/dist/analyzer.d.ts +2 -0
  7. package/dist/analyzer.js +1 -2
  8. package/dist/cli.cjs +525 -0
  9. package/dist/cli.cjs.map +1 -0
  10. package/dist/cli.d.cts +1 -0
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +526 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
  15. package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
  16. package/dist/engine-DPVLEHBi.js +163 -0
  17. package/dist/engine-DPVLEHBi.js.map +1 -0
  18. package/dist/engine-vqA-eL_T.cjs +186 -0
  19. package/dist/engine-vqA-eL_T.cjs.map +1 -0
  20. package/dist/gherkinParser-BT40q_i3.cjs +338 -0
  21. package/dist/gherkinParser-BT40q_i3.cjs.map +1 -0
  22. package/dist/gherkinParser-NcttZgN4.js +259 -0
  23. package/dist/gherkinParser-NcttZgN4.js.map +1 -0
  24. package/dist/hooks-BDCMKeNq.js +71 -0
  25. package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
  26. package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
  27. package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
  28. package/dist/runtime.cjs +7 -162
  29. package/dist/runtime.d.cts +44 -8
  30. package/dist/runtime.d.ts +44 -8
  31. package/dist/runtime.js +3 -159
  32. package/dist/step-forge.cjs +73 -216
  33. package/dist/step-forge.cjs.map +1 -1
  34. package/dist/step-forge.d.cts +19 -10
  35. package/dist/step-forge.d.ts +19 -10
  36. package/dist/step-forge.js +67 -185
  37. package/dist/step-forge.js.map +1 -1
  38. package/package.json +12 -18
  39. package/dist/analyzer-DJyJbU_V.js.map +0 -1
  40. package/dist/gherkinParser-Dp2d7JNr.js +0 -116
  41. package/dist/gherkinParser-Dp2d7JNr.js.map +0 -1
  42. package/dist/hooks-CywugMQQ.js +0 -82
  43. package/dist/runtime.cjs.map +0 -1
  44. package/dist/runtime.js.map +0 -1
  45. package/dist/vitest.d.ts +0 -74
  46. package/dist/vitest.js +0 -136
  47. package/dist/vitest.js.map +0 -1
@@ -1,82 +0,0 @@
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 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
48
- * runner reports it against the enclosing boundary.
49
- */
50
- async function runHooks(scope, timing, registry = globalHookRegistry) {
51
- for (const hook of registry.for(scope, timing)) await hook.fn();
52
- }
53
- const GLOBAL_GUARD = Symbol.for("step-forge.globalHooksStarted");
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.
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.
67
- */
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
- });
78
- }
79
- //#endregion
80
- export { StepRegistry as a, runHooks as i, ensureGlobalHooks as n, globalRegistry as o, globalHookRegistry as r, HookRegistry as t };
81
-
82
- //# sourceMappingURL=hooks-CywugMQQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.cjs","names":["ParameterTypeRegistry","ParameterType","CucumberExpression","globalHookRegistry"],"sources":["../../src/runtime/engine.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n CucumberExpression,\n ParameterType,\n ParameterTypeRegistry,\n} from \"@cucumber/cucumber-expressions\";\nimport { ParsedScenario, ParsedStep } from \"../analyzer/types\";\nimport { MergeableWorld } from \"../world\";\nimport { globalHookRegistry, HookRegistry, ScenarioHookFn } from \"./hooks\";\nimport { RegisteredStep, StepRegistry, StepType } from \"./registry\";\n\nconst keywordToStepType: Record<ParsedStep[\"effectiveKeyword\"], StepType> = {\n Given: \"given\",\n When: \"when\",\n Then: \"then\",\n};\n\n/** A registered step paired with its compiled Cucumber expression. */\ninterface CompiledStep {\n step: RegisteredStep;\n expression: CucumberExpression;\n}\n\nexport class UndefinedStepError extends Error {\n constructor(public readonly step: ParsedStep) {\n super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);\n this.name = \"UndefinedStepError\";\n }\n}\n\nexport class AmbiguousStepError extends Error {\n constructor(\n public readonly step: ParsedStep,\n public readonly matches: RegisteredStep[]\n ) {\n super(\n `Ambiguous step: \"${step.text}\" matched ${matches.length} definitions:\\n` +\n matches.map(m => ` - ${m.expression}`).join(\"\\n\")\n );\n this.name = \"AmbiguousStepError\";\n }\n}\n\nfunction compile(registry: StepRegistry): CompiledStep[] {\n return registry.all().map(step => {\n // Each step gets its own parameter-type registry (seeded with the\n // built-ins). A parser whose name is already registered — the built-in\n // `{int}`/`{float}`/`{string}`, or a repeat within the same step — reuses\n // that type; a novel name (e.g. `{boolean}`, `{color}`) is registered from\n // the parser's regexp + parse, so matching and coercion happen in one pass.\n const paramRegistry = new ParameterTypeRegistry();\n for (const parser of step.parsers) {\n if (paramRegistry.lookupByTypeName(parser.name)) continue;\n const regexps = Array.isArray(parser.regexp)\n ? parser.regexp\n : [parser.regexp];\n paramRegistry.defineParameterType(\n new ParameterType(parser.name, regexps, null, (value: string) =>\n parser.parse(value)\n )\n );\n }\n return {\n step,\n expression: new CucumberExpression(step.expression, paramRegistry),\n };\n });\n}\n\n/**\n * Find the single step definition matching a Gherkin step. Matching is\n * opinionated and strict: the keyword must line up with the step type, exactly\n * one definition must match, and undefined/ambiguous both throw rather than\n * silently skipping (unlike Cucumber's pending/undefined dance).\n */\nfunction matchStep(\n step: ParsedStep,\n compiled: CompiledStep[]\n): { step: RegisteredStep; args: unknown[] } {\n const expectedType = keywordToStepType[step.effectiveKeyword];\n const matches: { step: RegisteredStep; args: unknown[] }[] = [];\n\n for (const { step: def, expression } of compiled) {\n if (def.stepType !== expectedType) continue;\n const result = expression.match(step.text);\n if (result) {\n // The parsers are registered as the expression's parameter types, so the\n // captured values are already coerced (`{int}` → number, `{color}` → the\n // parser's T). `execute` consumes them as-is.\n matches.push({ step: def, args: result.map(a => a.getValue(null)) });\n }\n }\n\n if (matches.length === 0) throw new UndefinedStepError(step);\n if (matches.length > 1) {\n throw new AmbiguousStepError(\n step,\n matches.map(m => m.step)\n );\n }\n return matches[0];\n}\n\nexport interface StepResult {\n step: ParsedStep;\n status: \"passed\" | \"failed\" | \"skipped\";\n error?: Error;\n durationMs?: number;\n}\n\nexport interface ScenarioResult {\n scenario: ParsedScenario;\n status: \"passed\" | \"failed\";\n steps: StepResult[];\n}\n\n/**\n * Run one scenario against a registry. A fresh world is created per scenario\n * (state never leaks between scenarios). On the first failing step the\n * remaining steps are marked skipped, matching Cucumber's execution semantics.\n *\n * Throws on failure so it maps cleanly onto a test runner's `test()` body, but\n * the returned/attached `ScenarioResult` carries the per-step breakdown.\n */\nexport async function runScenario(\n scenario: ParsedScenario,\n registry: StepRegistry,\n makeWorld: () => MergeableWorld<any, any, any>,\n hooks: HookRegistry = globalHookRegistry\n): Promise<ScenarioResult> {\n const compiled = compile(registry);\n const world = makeWorld();\n const scenarioInfo = { name: scenario.name, file: scenario.file };\n const steps: StepResult[] = [];\n let failed = false;\n let firstError: Error | undefined;\n\n const fail = (err: unknown) => {\n if (failed) return;\n failed = true;\n firstError = err instanceof Error ? err : new Error(String(err));\n };\n\n // before-scenario hooks: a throw here aborts the scenario before any step.\n try {\n for (const hook of hooks.for(\"scenario\", \"before\")) {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n }\n } catch (err) {\n fail(err);\n }\n\n for (const step of scenario.steps) {\n if (failed) {\n steps.push({ step, status: \"skipped\" });\n continue;\n }\n try {\n const { step: def, args } = matchStep(step, compiled);\n await def.execute(world, args);\n steps.push({ step, status: \"passed\" });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n steps.push({ step, status: \"failed\", error });\n fail(error);\n }\n }\n\n // after-scenario hooks always run (teardown), even on failure. A hook failure\n // only becomes the scenario's error if nothing else failed first.\n for (const hook of hooks.for(\"scenario\", \"after\")) {\n try {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n } catch (err) {\n fail(err);\n }\n }\n\n const result: ScenarioResult = {\n scenario,\n status: failed ? \"failed\" : \"passed\",\n steps,\n };\n\n if (failed && firstError) {\n // Surface the failing Gherkin line as a real stack frame so the runner\n // renders a code frame from the `.feature` file itself. Hook failures have\n // no step to point at, so they surface with their own stack unchanged.\n const failing = steps.find(s => s.status === \"failed\");\n if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);\n throw firstError;\n }\n\n return result;\n}\n\n/**\n * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because\n * the frame's file is the real `.feature` on disk, the test runner treats it as\n * a source location and shows a code frame at the step — instead of us jamming\n * `file:line` into the error message. The frame goes *above* the real stack, so\n * the step-definition frames (the actual throw site) are preserved below it.\n */\nfunction attachFeatureFrame(\n error: Error,\n file: string,\n step: ParsedStep\n): void {\n const label = `${step.effectiveKeyword} ${step.text}`;\n const frame = ` at ${label} (${file}:${step.line}:${step.column})`;\n const stack = error.stack;\n if (!stack) {\n error.stack = `${error.name}: ${error.message}\\n${frame}`;\n return;\n }\n // A message can span multiple lines, so split on the first frame marker\n // rather than the first newline to find where the header ends.\n const firstFrame = stack.indexOf(\"\\n at \");\n if (firstFrame === -1) {\n error.stack = `${stack}\\n${frame}`;\n } else {\n error.stack =\n stack.slice(0, firstFrame) + `\\n${frame}` + stack.slice(firstFrame);\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.js","names":[],"sources":["../../src/runtime/engine.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n CucumberExpression,\n ParameterType,\n ParameterTypeRegistry,\n} from \"@cucumber/cucumber-expressions\";\nimport { ParsedScenario, ParsedStep } from \"../analyzer/types\";\nimport { MergeableWorld } from \"../world\";\nimport { globalHookRegistry, HookRegistry, ScenarioHookFn } from \"./hooks\";\nimport { RegisteredStep, StepRegistry, StepType } from \"./registry\";\n\nconst keywordToStepType: Record<ParsedStep[\"effectiveKeyword\"], StepType> = {\n Given: \"given\",\n When: \"when\",\n Then: \"then\",\n};\n\n/** A registered step paired with its compiled Cucumber expression. */\ninterface CompiledStep {\n step: RegisteredStep;\n expression: CucumberExpression;\n}\n\nexport class UndefinedStepError extends Error {\n constructor(public readonly step: ParsedStep) {\n super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);\n this.name = \"UndefinedStepError\";\n }\n}\n\nexport class AmbiguousStepError extends Error {\n constructor(\n public readonly step: ParsedStep,\n public readonly matches: RegisteredStep[]\n ) {\n super(\n `Ambiguous step: \"${step.text}\" matched ${matches.length} definitions:\\n` +\n matches.map(m => ` - ${m.expression}`).join(\"\\n\")\n );\n this.name = \"AmbiguousStepError\";\n }\n}\n\nfunction compile(registry: StepRegistry): CompiledStep[] {\n return registry.all().map(step => {\n // Each step gets its own parameter-type registry (seeded with the\n // built-ins). A parser whose name is already registered — the built-in\n // `{int}`/`{float}`/`{string}`, or a repeat within the same step — reuses\n // that type; a novel name (e.g. `{boolean}`, `{color}`) is registered from\n // the parser's regexp + parse, so matching and coercion happen in one pass.\n const paramRegistry = new ParameterTypeRegistry();\n for (const parser of step.parsers) {\n if (paramRegistry.lookupByTypeName(parser.name)) continue;\n const regexps = Array.isArray(parser.regexp)\n ? parser.regexp\n : [parser.regexp];\n paramRegistry.defineParameterType(\n new ParameterType(parser.name, regexps, null, (value: string) =>\n parser.parse(value)\n )\n );\n }\n return {\n step,\n expression: new CucumberExpression(step.expression, paramRegistry),\n };\n });\n}\n\n/**\n * Find the single step definition matching a Gherkin step. Matching is\n * opinionated and strict: the keyword must line up with the step type, exactly\n * one definition must match, and undefined/ambiguous both throw rather than\n * silently skipping (unlike Cucumber's pending/undefined dance).\n */\nfunction matchStep(\n step: ParsedStep,\n compiled: CompiledStep[]\n): { step: RegisteredStep; args: unknown[] } {\n const expectedType = keywordToStepType[step.effectiveKeyword];\n const matches: { step: RegisteredStep; args: unknown[] }[] = [];\n\n for (const { step: def, expression } of compiled) {\n if (def.stepType !== expectedType) continue;\n const result = expression.match(step.text);\n if (result) {\n // The parsers are registered as the expression's parameter types, so the\n // captured values are already coerced (`{int}` → number, `{color}` → the\n // parser's T). `execute` consumes them as-is.\n matches.push({ step: def, args: result.map(a => a.getValue(null)) });\n }\n }\n\n if (matches.length === 0) throw new UndefinedStepError(step);\n if (matches.length > 1) {\n throw new AmbiguousStepError(\n step,\n matches.map(m => m.step)\n );\n }\n return matches[0];\n}\n\nexport interface StepResult {\n step: ParsedStep;\n status: \"passed\" | \"failed\" | \"skipped\";\n error?: Error;\n durationMs?: number;\n}\n\nexport interface ScenarioResult {\n scenario: ParsedScenario;\n status: \"passed\" | \"failed\";\n steps: StepResult[];\n}\n\n/**\n * Run one scenario against a registry. A fresh world is created per scenario\n * (state never leaks between scenarios). On the first failing step the\n * remaining steps are marked skipped, matching Cucumber's execution semantics.\n *\n * Throws on failure so it maps cleanly onto a test runner's `test()` body, but\n * the returned/attached `ScenarioResult` carries the per-step breakdown.\n */\nexport async function runScenario(\n scenario: ParsedScenario,\n registry: StepRegistry,\n makeWorld: () => MergeableWorld<any, any, any>,\n hooks: HookRegistry = globalHookRegistry\n): Promise<ScenarioResult> {\n const compiled = compile(registry);\n const world = makeWorld();\n const scenarioInfo = { name: scenario.name, file: scenario.file };\n const steps: StepResult[] = [];\n let failed = false;\n let firstError: Error | undefined;\n\n const fail = (err: unknown) => {\n if (failed) return;\n failed = true;\n firstError = err instanceof Error ? err : new Error(String(err));\n };\n\n // before-scenario hooks: a throw here aborts the scenario before any step.\n try {\n for (const hook of hooks.for(\"scenario\", \"before\")) {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n }\n } catch (err) {\n fail(err);\n }\n\n for (const step of scenario.steps) {\n if (failed) {\n steps.push({ step, status: \"skipped\" });\n continue;\n }\n try {\n const { step: def, args } = matchStep(step, compiled);\n await def.execute(world, args);\n steps.push({ step, status: \"passed\" });\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n steps.push({ step, status: \"failed\", error });\n fail(error);\n }\n }\n\n // after-scenario hooks always run (teardown), even on failure. A hook failure\n // only becomes the scenario's error if nothing else failed first.\n for (const hook of hooks.for(\"scenario\", \"after\")) {\n try {\n await (hook.fn as ScenarioHookFn)({ world, scenario: scenarioInfo });\n } catch (err) {\n fail(err);\n }\n }\n\n const result: ScenarioResult = {\n scenario,\n status: failed ? \"failed\" : \"passed\",\n steps,\n };\n\n if (failed && firstError) {\n // Surface the failing Gherkin line as a real stack frame so the runner\n // renders a code frame from the `.feature` file itself. Hook failures have\n // no step to point at, so they surface with their own stack unchanged.\n const failing = steps.find(s => s.status === \"failed\");\n if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);\n throw firstError;\n }\n\n return result;\n}\n\n/**\n * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because\n * the frame's file is the real `.feature` on disk, the test runner treats it as\n * a source location and shows a code frame at the step — instead of us jamming\n * `file:line` into the error message. The frame goes *above* the real stack, so\n * the step-definition frames (the actual throw site) are preserved below it.\n */\nfunction attachFeatureFrame(\n error: Error,\n file: string,\n step: ParsedStep\n): void {\n const label = `${step.effectiveKeyword} ${step.text}`;\n const frame = ` at ${label} (${file}:${step.line}:${step.column})`;\n const stack = error.stack;\n if (!stack) {\n error.stack = `${error.name}: ${error.message}\\n${frame}`;\n return;\n }\n // A message can span multiple lines, so split on the first frame marker\n // rather than the first newline to find where the header ends.\n const firstFrame = stack.indexOf(\"\\n at \");\n if (firstFrame === -1) {\n error.stack = `${stack}\\n${frame}`;\n } else {\n error.stack =\n stack.slice(0, firstFrame) + `\\n${frame}` + stack.slice(firstFrame);\n }\n}\n"],"mappings":";;;AAWA,MAAM,oBAAsE;CAC1E,OAAO;CACP,MAAM;CACN,MAAM;AACR;AAQA,IAAa,qBAAb,cAAwC,MAAM;CAChB;CAA5B,YAAY,MAAkC;EAC5C,MAAM,mBAAmB,KAAK,iBAAiB,GAAG,KAAK,MAAM;EADnC,KAAA,OAAA;EAE1B,KAAK,OAAO;CACd;AACF;AAEA,IAAa,qBAAb,cAAwC,MAAM;CAE1B;CACA;CAFlB,YACE,MACA,SACA;EACA,MACE,oBAAoB,KAAK,KAAK,YAAY,QAAQ,OAAO,mBACvD,QAAQ,KAAI,MAAK,OAAO,EAAE,YAAY,CAAC,CAAC,KAAK,IAAI,CACrD;EANgB,KAAA,OAAA;EACA,KAAA,UAAA;EAMhB,KAAK,OAAO;CACd;AACF;AAEA,SAAS,QAAQ,UAAwC;CACvD,OAAO,SAAS,IAAI,CAAC,CAAC,KAAI,SAAQ;EAMhC,MAAM,gBAAgB,IAAI,sBAAsB;EAChD,KAAK,MAAM,UAAU,KAAK,SAAS;GACjC,IAAI,cAAc,iBAAiB,OAAO,IAAI,GAAG;GACjD,MAAM,UAAU,MAAM,QAAQ,OAAO,MAAM,IACvC,OAAO,SACP,CAAC,OAAO,MAAM;GAClB,cAAc,oBACZ,IAAI,cAAc,OAAO,MAAM,SAAS,OAAO,UAC7C,OAAO,MAAM,KAAK,CACpB,CACF;EACF;EACA,OAAO;GACL;GACA,YAAY,IAAI,mBAAmB,KAAK,YAAY,aAAa;EACnE;CACF,CAAC;AACH;;;;;;;AAQA,SAAS,UACP,MACA,UAC2C;CAC3C,MAAM,eAAe,kBAAkB,KAAK;CAC5C,MAAM,UAAuD,CAAC;CAE9D,KAAK,MAAM,EAAE,MAAM,KAAK,gBAAgB,UAAU;EAChD,IAAI,IAAI,aAAa,cAAc;EACnC,MAAM,SAAS,WAAW,MAAM,KAAK,IAAI;EACzC,IAAI,QAIF,QAAQ,KAAK;GAAE,MAAM;GAAK,MAAM,OAAO,KAAI,MAAK,EAAE,SAAS,IAAI,CAAC;EAAE,CAAC;CAEvE;CAEA,IAAI,QAAQ,WAAW,GAAG,MAAM,IAAI,mBAAmB,IAAI;CAC3D,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,mBACR,MACA,QAAQ,KAAI,MAAK,EAAE,IAAI,CACzB;CAEF,OAAO,QAAQ;AACjB;;;;;;;;;AAuBA,eAAsB,YACpB,UACA,UACA,WACA,QAAsB,oBACG;CACzB,MAAM,WAAW,QAAQ,QAAQ;CACjC,MAAM,QAAQ,UAAU;CACxB,MAAM,eAAe;EAAE,MAAM,SAAS;EAAM,MAAM,SAAS;CAAK;CAChE,MAAM,QAAsB,CAAC;CAC7B,IAAI,SAAS;CACb,IAAI;CAEJ,MAAM,QAAQ,QAAiB;EAC7B,IAAI,QAAQ;EACZ,SAAS;EACT,aAAa,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;CACjE;CAGA,IAAI;EACF,KAAK,MAAM,QAAQ,MAAM,IAAI,YAAY,QAAQ,GAC/C,MAAO,KAAK,GAAsB;GAAE;GAAO,UAAU;EAAa,CAAC;CAEvE,SAAS,KAAK;EACZ,KAAK,GAAG;CACV;CAEA,KAAK,MAAM,QAAQ,SAAS,OAAO;EACjC,IAAI,QAAQ;GACV,MAAM,KAAK;IAAE;IAAM,QAAQ;GAAU,CAAC;GACtC;EACF;EACA,IAAI;GACF,MAAM,EAAE,MAAM,KAAK,SAAS,UAAU,MAAM,QAAQ;GACpD,MAAM,IAAI,QAAQ,OAAO,IAAI;GAC7B,MAAM,KAAK;IAAE;IAAM,QAAQ;GAAS,CAAC;EACvC,SAAS,KAAK;GACZ,MAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GAChE,MAAM,KAAK;IAAE;IAAM,QAAQ;IAAU;GAAM,CAAC;GAC5C,KAAK,KAAK;EACZ;CACF;CAIA,KAAK,MAAM,QAAQ,MAAM,IAAI,YAAY,OAAO,GAC9C,IAAI;EACF,MAAO,KAAK,GAAsB;GAAE;GAAO,UAAU;EAAa,CAAC;CACrE,SAAS,KAAK;EACZ,KAAK,GAAG;CACV;CAGF,MAAM,SAAyB;EAC7B;EACA,QAAQ,SAAS,WAAW;EAC5B;CACF;CAEA,IAAI,UAAU,YAAY;EAIxB,MAAM,UAAU,MAAM,MAAK,MAAK,EAAE,WAAW,QAAQ;EACrD,IAAI,SAAS,mBAAmB,YAAY,SAAS,MAAM,QAAQ,IAAI;EACvE,MAAM;CACR;CAEA,OAAO;AACT;;;;;;;;AASA,SAAS,mBACP,OACA,MACA,MACM;CAEN,MAAM,QAAQ,UAAU,GADP,KAAK,iBAAiB,GAAG,KAAK,OACjB,IAAI,KAAK,GAAG,KAAK,KAAK,GAAG,KAAK,OAAO;CACnE,MAAM,QAAQ,MAAM;CACpB,IAAI,CAAC,OAAO;EACV,MAAM,QAAQ,GAAG,MAAM,KAAK,IAAI,MAAM,QAAQ,IAAI;EAClD;CACF;CAGA,MAAM,aAAa,MAAM,QAAQ,WAAW;CAC5C,IAAI,eAAe,IACjB,MAAM,QAAQ,GAAG,MAAM,IAAI;MAE3B,MAAM,QACJ,MAAM,MAAM,GAAG,UAAU,IAAI,KAAK,UAAU,MAAM,MAAM,UAAU;AAExE"}
package/dist/vitest.d.ts DELETED
@@ -1,74 +0,0 @@
1
- //#region src/runtime/vitest.d.ts
2
- interface StepForgeOptions {
3
- /**
4
- * Globs (relative to the Vite root) for step-definition modules.
5
- * Defaults to `**​/*.steps.ts`.
6
- */
7
- steps?: string | string[];
8
- /**
9
- * Module that default-exports a world factory `() => world`, resolved
10
- * relative to the Vite root. When omitted, each scenario gets a fresh
11
- * `BasicWorld`.
12
- */
13
- world?: string;
14
- /**
15
- * Feature-file glob(s) to register as test files. Defaults to
16
- * `**​/*.feature`. Accepts an array to scope several exact files or patterns
17
- * (e.g. running real features while excluding analyzer fixtures).
18
- */
19
- features?: string | string[];
20
- /**
21
- * Advanced: module specifier the generated tests import the runtime from.
22
- * Defaults to the published `@step-forge/step-forge/runtime` entry; override
23
- * only when consuming the library from source (e.g. this repo's own tests).
24
- */
25
- runtimeModule?: string;
26
- /**
27
- * Advanced: module specifier for the library core (used for the default
28
- * `BasicWorld`). Defaults to `@step-forge/step-forge`.
29
- */
30
- coreModule?: string;
31
- }
32
- /** Minimal Vite plugin shape (avoids a hard dependency on vite's types). */
33
- interface VitePlugin {
34
- name: string;
35
- enforce?: "pre" | "post";
36
- config?: () => unknown;
37
- configResolved?: (config: {
38
- root: string;
39
- }) => void;
40
- transform?: (code: string, id: string) => Promise<{
41
- code: string;
42
- map: null;
43
- } | undefined>;
44
- }
45
- /**
46
- * Vite/Vitest plugin: compiles each `.feature` file into a test module so
47
- * scenarios show up as native Vitest tests (watch mode, --ui, per-scenario
48
- * tasks, the lot). The transform injects imports of the step modules so that
49
- * (a) steps self-register into the same registry the engine reads, and
50
- * (b) Vite's HMR graph invalidates the feature test when a step file changes.
51
- */
52
- declare function stepForge(options?: StepForgeOptions): VitePlugin;
53
- /**
54
- * One-line Vitest config preset. Drop this in `vitest.config.ts`:
55
- *
56
- * ```ts
57
- * import { defineStepForgeConfig } from "@step-forge/step-forge/vitest";
58
- * export default defineStepForgeConfig({ world: "./support/world.ts" });
59
- * ```
60
- *
61
- * Runs `**​/*.feature` as native tests, auto-discovers `**​/*.steps.ts`, and
62
- * defaults the world to `BasicWorld`. Override any Vitest option via `test`.
63
- */
64
- declare function defineStepForgeConfig(options?: StepForgeOptions & {
65
- test?: Record<string, unknown>;
66
- }): {
67
- plugins: VitePlugin[];
68
- test: {
69
- include: string[];
70
- };
71
- };
72
- //#endregion
73
- export { StepForgeOptions, defineStepForgeConfig, stepForge };
74
- //# sourceMappingURL=vitest.d.ts.map
package/dist/vitest.js DELETED
@@ -1,136 +0,0 @@
1
- import { t as parseFeatureContent } from "./gherkinParser-Dp2d7JNr.js";
2
- import { glob } from "node:fs/promises";
3
- import * as path from "node:path";
4
- //#region src/runtime/vitest.ts
5
- const DEFAULT_STEPS = "**/*.steps.ts";
6
- const DEFAULT_FEATURES = "**/*.feature";
7
- const DEFAULT_RUNTIME_MODULE = "@step-forge/step-forge/runtime";
8
- const DEFAULT_CORE_MODULE = "@step-forge/step-forge";
9
- function toArray(value) {
10
- return Array.isArray(value) ? value : [value];
11
- }
12
- /**
13
- * Map a scenario's Gherkin tags onto the Vitest test variant. `@skip` wins over
14
- * `@only` (a scenario explicitly skipped stays skipped even if also `@only`ed),
15
- * and everything else is a plain `test`.
16
- */
17
- function testFn(tags) {
18
- if (tags.includes("@skip")) return "test.skip";
19
- if (tags.includes("@only")) return "test.only";
20
- return "test";
21
- }
22
- /** One `runScenario` test line for the scenario at `__scenarios[index]`. */
23
- function testLine(scenario, index, indent) {
24
- return `${indent}${testFn(scenario.tags)}(${JSON.stringify(scenario.name)}, () => runScenario(__scenarios[${index}], globalRegistry, __makeWorld));`;
25
- }
26
- /**
27
- * Emit the test bodies for a feature. Plain scenarios become one `test` each;
28
- * consecutive rows expanded from the same `Scenario Outline` are wrapped in a
29
- * `describe` named after the outline, so they read as one labelled group with a
30
- * `test` per example row (and each row keeps its own `@skip`/`@only`).
31
- */
32
- function generateTests(scenarios) {
33
- const lines = [];
34
- let i = 0;
35
- while (i < scenarios.length) {
36
- const outlineName = scenarios[i].outline?.name;
37
- if (outlineName === void 0) {
38
- lines.push(testLine(scenarios[i], i, " "));
39
- i += 1;
40
- continue;
41
- }
42
- lines.push(` describe(${JSON.stringify(outlineName)}, () => {`);
43
- while (i < scenarios.length && scenarios[i].outline?.name === outlineName) {
44
- lines.push(testLine(scenarios[i], i, " "));
45
- i += 1;
46
- }
47
- lines.push(` });`);
48
- }
49
- return lines.join("\n");
50
- }
51
- function toSpecifier(p) {
52
- return path.isAbsolute(p) ? p.split(path.sep).join("/") : p;
53
- }
54
- async function resolveSteps(steps, root) {
55
- const patterns = Array.isArray(steps) ? steps : [steps];
56
- const files = /* @__PURE__ */ new Set();
57
- for (const pattern of patterns) for await (const file of glob(pattern, { cwd: root })) files.add(path.resolve(root, file));
58
- return [...files];
59
- }
60
- /**
61
- * Vite/Vitest plugin: compiles each `.feature` file into a test module so
62
- * scenarios show up as native Vitest tests (watch mode, --ui, per-scenario
63
- * tasks, the lot). The transform injects imports of the step modules so that
64
- * (a) steps self-register into the same registry the engine reads, and
65
- * (b) Vite's HMR graph invalidates the feature test when a step file changes.
66
- */
67
- function stepForge(options = {}) {
68
- const featuresGlobs = toArray(options.features ?? DEFAULT_FEATURES);
69
- const stepsGlob = options.steps ?? DEFAULT_STEPS;
70
- const runtimeModule = options.runtimeModule ?? DEFAULT_RUNTIME_MODULE;
71
- const coreModule = options.coreModule ?? DEFAULT_CORE_MODULE;
72
- let root = process.cwd();
73
- return {
74
- name: "step-forge",
75
- enforce: "pre",
76
- config() {
77
- return { test: { include: featuresGlobs } };
78
- },
79
- configResolved(config) {
80
- root = config.root;
81
- },
82
- async transform(code, id) {
83
- if (!id.endsWith(".feature")) return;
84
- const scenarios = parseFeatureContent(code, id);
85
- const featureName = /^\s*Feature:\s*(.+)$/m.exec(code)?.[1]?.trim() ?? path.basename(id);
86
- const stepImports = (await resolveSteps(stepsGlob, root)).map((f) => `import ${JSON.stringify(toSpecifier(f))};`).join("\n");
87
- const worldImport = options.world ? `import __makeWorld from ${JSON.stringify(toSpecifier(path.resolve(root, options.world)))};` : `import { BasicWorld } from ${JSON.stringify(coreModule)};\nconst __makeWorld = () => new BasicWorld();`;
88
- const tests = generateTests(scenarios);
89
- return {
90
- code: `
91
- import { describe, test, beforeAll, afterAll } from "vitest";
92
- import { runScenario, globalRegistry, runHooks, ensureGlobalHooks } from ${JSON.stringify(runtimeModule)};
93
- ${worldImport}
94
- ${stepImports}
95
-
96
- const __scenarios = ${JSON.stringify(scenarios)};
97
-
98
- describe(${JSON.stringify(featureName)}, () => {
99
- // Global before-hooks fire once per worker, ahead of feature hooks.
100
- beforeAll(() => ensureGlobalHooks());
101
- beforeAll(() => runHooks("feature", "before"));
102
- afterAll(() => runHooks("feature", "after"));
103
- ${tests}
104
- });
105
- `,
106
- map: null
107
- };
108
- }
109
- };
110
- }
111
- /**
112
- * One-line Vitest config preset. Drop this in `vitest.config.ts`:
113
- *
114
- * ```ts
115
- * import { defineStepForgeConfig } from "@step-forge/step-forge/vitest";
116
- * export default defineStepForgeConfig({ world: "./support/world.ts" });
117
- * ```
118
- *
119
- * Runs `**​/*.feature` as native tests, auto-discovers `**​/*.steps.ts`, and
120
- * defaults the world to `BasicWorld`. Override any Vitest option via `test`.
121
- */
122
- function defineStepForgeConfig(options = {}) {
123
- const { test, ...pluginOptions } = options;
124
- const featuresGlobs = toArray(pluginOptions.features ?? DEFAULT_FEATURES);
125
- return {
126
- plugins: [stepForge(pluginOptions)],
127
- test: {
128
- include: featuresGlobs,
129
- ...test
130
- }
131
- };
132
- }
133
- //#endregion
134
- export { defineStepForgeConfig, stepForge };
135
-
136
- //# sourceMappingURL=vitest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"vitest.js","names":[],"sources":["../../src/runtime/vitest.ts"],"sourcesContent":["import { glob } from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { parseFeatureContent } from \"../analyzer/gherkinParser\";\nimport { ParsedScenario } from \"../analyzer/types\";\n\nexport interface StepForgeOptions {\n /**\n * Globs (relative to the Vite root) for step-definition modules.\n * Defaults to `**​/*.steps.ts`.\n */\n steps?: string | string[];\n /**\n * Module that default-exports a world factory `() => world`, resolved\n * relative to the Vite root. When omitted, each scenario gets a fresh\n * `BasicWorld`.\n */\n world?: string;\n /**\n * Feature-file glob(s) to register as test files. Defaults to\n * `**​/*.feature`. Accepts an array to scope several exact files or patterns\n * (e.g. running real features while excluding analyzer fixtures).\n */\n features?: string | string[];\n /**\n * Advanced: module specifier the generated tests import the runtime from.\n * Defaults to the published `@step-forge/step-forge/runtime` entry; override\n * only when consuming the library from source (e.g. this repo's own tests).\n */\n runtimeModule?: string;\n /**\n * Advanced: module specifier for the library core (used for the default\n * `BasicWorld`). Defaults to `@step-forge/step-forge`.\n */\n coreModule?: string;\n}\n\n/** Minimal Vite plugin shape (avoids a hard dependency on vite's types). */\ninterface VitePlugin {\n name: string;\n enforce?: \"pre\" | \"post\";\n config?: () => unknown;\n configResolved?: (config: { root: string }) => void;\n transform?: (\n code: string,\n id: string\n ) => Promise<{ code: string; map: null } | undefined>;\n}\n\nconst DEFAULT_STEPS = \"**/*.steps.ts\";\nconst DEFAULT_FEATURES = \"**/*.feature\";\nconst DEFAULT_RUNTIME_MODULE = \"@step-forge/step-forge/runtime\";\nconst DEFAULT_CORE_MODULE = \"@step-forge/step-forge\";\n\nfunction toArray<T>(value: T | T[]): T[] {\n return Array.isArray(value) ? value : [value];\n}\n\n/**\n * Map a scenario's Gherkin tags onto the Vitest test variant. `@skip` wins over\n * `@only` (a scenario explicitly skipped stays skipped even if also `@only`ed),\n * and everything else is a plain `test`.\n */\nfunction testFn(tags: string[]): string {\n if (tags.includes(\"@skip\")) return \"test.skip\";\n if (tags.includes(\"@only\")) return \"test.only\";\n return \"test\";\n}\n\n/** One `runScenario` test line for the scenario at `__scenarios[index]`. */\nfunction testLine(\n scenario: ParsedScenario,\n index: number,\n indent: string\n): string {\n return (\n `${indent}${testFn(scenario.tags)}(${JSON.stringify(scenario.name)}, () => ` +\n `runScenario(__scenarios[${index}], globalRegistry, __makeWorld));`\n );\n}\n\n/**\n * Emit the test bodies for a feature. Plain scenarios become one `test` each;\n * consecutive rows expanded from the same `Scenario Outline` are wrapped in a\n * `describe` named after the outline, so they read as one labelled group with a\n * `test` per example row (and each row keeps its own `@skip`/`@only`).\n */\nfunction generateTests(scenarios: ParsedScenario[]): string {\n const lines: string[] = [];\n let i = 0;\n while (i < scenarios.length) {\n const outlineName = scenarios[i].outline?.name;\n if (outlineName === undefined) {\n lines.push(testLine(scenarios[i], i, \" \"));\n i += 1;\n continue;\n }\n lines.push(` describe(${JSON.stringify(outlineName)}, () => {`);\n while (i < scenarios.length && scenarios[i].outline?.name === outlineName) {\n lines.push(testLine(scenarios[i], i, \" \"));\n i += 1;\n }\n lines.push(` });`);\n }\n return lines.join(\"\\n\");\n}\n\nfunction toSpecifier(p: string): string {\n // Absolute filesystem paths must be POSIX-style for the generated imports;\n // bare package specifiers are emitted verbatim.\n return path.isAbsolute(p) ? p.split(path.sep).join(\"/\") : p;\n}\n\nasync function resolveSteps(\n steps: string | string[],\n root: string\n): Promise<string[]> {\n const patterns = Array.isArray(steps) ? steps : [steps];\n const files = new Set<string>();\n for (const pattern of patterns) {\n for await (const file of glob(pattern, { cwd: root })) {\n files.add(path.resolve(root, file));\n }\n }\n return [...files];\n}\n\n/**\n * Vite/Vitest plugin: compiles each `.feature` file into a test module so\n * scenarios show up as native Vitest tests (watch mode, --ui, per-scenario\n * tasks, the lot). The transform injects imports of the step modules so that\n * (a) steps self-register into the same registry the engine reads, and\n * (b) Vite's HMR graph invalidates the feature test when a step file changes.\n */\nexport function stepForge(options: StepForgeOptions = {}): VitePlugin {\n const featuresGlobs = toArray(options.features ?? DEFAULT_FEATURES);\n const stepsGlob = options.steps ?? DEFAULT_STEPS;\n const runtimeModule = options.runtimeModule ?? DEFAULT_RUNTIME_MODULE;\n const coreModule = options.coreModule ?? DEFAULT_CORE_MODULE;\n let root = process.cwd();\n\n return {\n name: \"step-forge\",\n enforce: \"pre\",\n config() {\n return { test: { include: featuresGlobs } };\n },\n configResolved(config) {\n root = config.root;\n },\n async transform(code, id) {\n if (!id.endsWith(\".feature\")) return;\n\n const scenarios = parseFeatureContent(code, id);\n const featureName =\n /^\\s*Feature:\\s*(.+)$/m.exec(code)?.[1]?.trim() ?? path.basename(id);\n const stepFiles = await resolveSteps(stepsGlob, root);\n\n const stepImports = stepFiles\n .map(f => `import ${JSON.stringify(toSpecifier(f))};`)\n .join(\"\\n\");\n\n // World factory: an explicit `world` module, or a fresh BasicWorld.\n const worldImport = options.world\n ? `import __makeWorld from ${JSON.stringify(\n toSpecifier(path.resolve(root, options.world))\n )};`\n : `import { BasicWorld } from ${JSON.stringify(coreModule)};\\n` +\n `const __makeWorld = () => new BasicWorld();`;\n\n const tests = generateTests(scenarios);\n\n const generated = `\nimport { describe, test, beforeAll, afterAll } from \"vitest\";\nimport { runScenario, globalRegistry, runHooks, ensureGlobalHooks } from ${JSON.stringify(\n runtimeModule\n )};\n${worldImport}\n${stepImports}\n\nconst __scenarios = ${JSON.stringify(scenarios)};\n\ndescribe(${JSON.stringify(featureName)}, () => {\n // Global before-hooks fire once per worker, ahead of feature hooks.\n beforeAll(() => ensureGlobalHooks());\n beforeAll(() => runHooks(\"feature\", \"before\"));\n afterAll(() => runHooks(\"feature\", \"after\"));\n${tests}\n});\n`;\n return { code: generated, map: null };\n },\n };\n}\n\n/**\n * One-line Vitest config preset. Drop this in `vitest.config.ts`:\n *\n * ```ts\n * import { defineStepForgeConfig } from \"@step-forge/step-forge/vitest\";\n * export default defineStepForgeConfig({ world: \"./support/world.ts\" });\n * ```\n *\n * Runs `**​/*.feature` as native tests, auto-discovers `**​/*.steps.ts`, and\n * defaults the world to `BasicWorld`. Override any Vitest option via `test`.\n */\nexport function defineStepForgeConfig(\n options: StepForgeOptions & { test?: Record<string, unknown> } = {}\n) {\n const { test, ...pluginOptions } = options;\n const featuresGlobs = toArray(pluginOptions.features ?? DEFAULT_FEATURES);\n return {\n plugins: [stepForge(pluginOptions)],\n test: { include: featuresGlobs, ...test },\n };\n}\n"],"mappings":"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"}