@step-forge/step-forge 0.0.20 → 0.0.21

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gherkinParser-CHpkEwii.cjs","names":["_","path","fs","Parser","AstBuilder","messages","GherkinClassicTokenMatcher"],"sources":["../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["import _ from \"lodash\";\n\nexport type WorldState<State> = {\n readonly [K in keyof State]?: State[K];\n};\n\nexport type MergeableWorldState<T> = WorldState<T> & {\n merge: (newState: Partial<T>) => void;\n};\n\nfunction mergeCustomizer(objValue: unknown, srcValue: unknown) {\n if (_.isArray(objValue)) {\n return objValue.concat(srcValue);\n } else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) {\n throw new Error(\n `Merge would have destroyed previous value ${objValue} with ${srcValue}`\n );\n }\n return objValue;\n}\n\nexport const createMergeableState = <T>(\n state: WorldState<T>\n): MergeableWorldState<T> => {\n return {\n ...state,\n merge: (newState: Partial<T>) => {\n state = _.merge({ ...state }, newState, mergeCustomizer);\n },\n };\n};\n\nexport type MergeableWorld<Given, When, Then> = {\n given: MergeableWorldState<Given>;\n when: MergeableWorldState<When>;\n then: MergeableWorldState<Then>;\n};\n\nexport class BasicWorld<Given, When, Then> {\n private givenState: WorldState<Given> = {};\n private whenState: WorldState<When> = {};\n private thenState: WorldState<Then> = {};\n\n public get given(): MergeableWorldState<Given> {\n return {\n ...this.givenState,\n merge: (newState: Partial<Given>) => {\n this.givenState = _.merge(\n { ...this.givenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get when(): MergeableWorldState<When> {\n return {\n ...this.whenState,\n merge: (newState: Partial<When>) => {\n this.whenState = _.merge(\n { ...this.whenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get then(): MergeableWorldState<Then> {\n return {\n ...this.thenState,\n merge: (newState: Partial<Then>) => {\n this.thenState = _.merge(\n { ...this.thenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n}\n\nexport const createBasicWorld = <Given, When, Then>(): MergeableWorld<\n Given,\n When,\n Then\n> => {\n return new BasicWorld<Given, When, Then>();\n};\n","import { glob } from \"node:fs/promises\";\nimport { stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\n/** Glob metacharacters. A pattern with none of these is a literal path. */\nconst MAGIC = /[*?[\\]{}!()]/;\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve glob patterns to a de-duplicated list of absolute file paths, with one\n * important portability guarantee: a **literal absolute path** (no glob magic)\n * is returned directly if it exists, without being handed to `glob()`.\n *\n * This exists because `node:fs`'s `glob` diverges between runtimes — Node\n * matches an absolute-path pattern, Bun returns nothing for one. Rather than\n * depend on that behaviour, we only ever glob relative patterns (against `cwd`)\n * and short-circuit concrete absolute paths ourselves, so callers get identical\n * results under Node and Bun.\n */\nexport async function globFiles(\n patterns: string[],\n cwd: string = process.cwd()\n): Promise<string[]> {\n const files = new Set<string>();\n for (const pattern of patterns) {\n if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {\n if (await isFile(pattern)) files.add(pattern);\n continue;\n }\n for await (const match of glob(pattern, { cwd })) {\n files.add(path.resolve(cwd, match));\n }\n }\n return [...files];\n}\n","import * as fs from \"node:fs\";\nimport { GherkinClassicTokenMatcher, Parser, AstBuilder } from \"@cucumber/gherkin\";\nimport * as messages from \"@cucumber/messages\";\nimport { ParsedScenario, ParsedStep } from \"./types.js\";\n\ntype GherkinKeyword = \"Given\" | \"When\" | \"Then\" | \"And\" | \"But\";\n\nexport function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {\n const scenarios: ParsedScenario[] = [];\n\n for (const filePath of filePaths) {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const parsed = parseFeatureContent(content, filePath);\n scenarios.push(...parsed);\n }\n\n return scenarios;\n}\n\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n const newId = messages.IdGenerator.uuid();\n const builder = new AstBuilder(newId);\n const matcher = new GherkinClassicTokenMatcher();\n const parser = new Parser(builder, matcher);\n\n const gherkinDocument: messages.GherkinDocument = parser.parse(content);\n const feature = gherkinDocument.feature;\n if (!feature) return [];\n\n // Collect background steps at the feature level\n const featureBackground: messages.Step[] = [];\n const scenarios: ParsedScenario[] = [];\n const featureTags = tagNames(feature.tags);\n\n for (const child of feature.children) {\n if (child.background) {\n featureBackground.push(...child.background.steps);\n }\n\n if (child.scenario) {\n scenarios.push(\n ...expandScenario(\n child.scenario,\n featureBackground,\n filePath,\n featureTags\n )\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds and tags, both inherited by the\n // rule's scenarios.\n const ruleBackground: messages.Step[] = [...featureBackground];\n const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];\n for (const ruleChild of child.rule.children) {\n if (ruleChild.background) {\n ruleBackground.push(...ruleChild.background.steps);\n }\n if (ruleChild.scenario) {\n scenarios.push(\n ...expandScenario(\n ruleChild.scenario,\n ruleBackground,\n filePath,\n ruleTags\n )\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\n/** Extract tag names (each keeping its leading `@`), deduped in order. */\nfunction tagNames(tags: readonly messages.Tag[] | undefined): string[] {\n return [...new Set((tags ?? []).map(t => t.name))];\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string,\n inheritedTags: string[]\n): ParsedScenario[] {\n const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];\n const hasExamples =\n scenario.examples.length > 0 &&\n scenario.examples.some((e) => e.tableBody.length > 0);\n\n if (!hasExamples) {\n // Regular scenario\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioParsed = convertSteps(scenario.steps);\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);\n\n return [\n {\n name: scenario.name,\n file: filePath,\n steps: allSteps,\n tags: scenarioTags,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row. Rows carry the outline's\n // base name so the runner can group them, plus the Examples-block tags.\n const results: ParsedScenario[] = [];\n for (const example of scenario.examples) {\n if (!example.tableHeader || example.tableBody.length === 0) continue;\n const headers = example.tableHeader.cells.map((c) => c.value);\n const exampleTags = [...scenarioTags, ...tagNames(example.tags)];\n\n for (const row of example.tableBody) {\n const values = row.cells.map((c) => c.value);\n const substitution: Record<string, string> = {};\n headers.forEach((h, i) => {\n substitution[h] = values[i];\n });\n\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioSteps = convertSteps(scenario.steps).map((step) => ({\n ...step,\n text: substituteExampleValues(step.text, substitution),\n }));\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);\n\n results.push({\n name: headers.map((h, i) => `${h}=${values[i]}`).join(\", \"),\n file: filePath,\n steps: allSteps,\n tags: exampleTags,\n outline: { name: scenario.name },\n });\n }\n }\n\n return results;\n}\n\nfunction convertSteps(\n steps: readonly messages.Step[]\n): Omit<ParsedStep, \"effectiveKeyword\">[] {\n return steps.map((step) => ({\n keyword: normalizeKeyword(step.keyword),\n text: step.text,\n line: step.location.line,\n // step.location.column points to the keyword start; shift past the\n // keyword (which includes a trailing space) so column points to the\n // start of the step text. This makes `column + text.length` produce\n // the correct end position for diagnostic ranges.\n column: (step.location.column ?? 1) + step.keyword.length,\n }));\n}\n\nfunction normalizeKeyword(keyword: string): GherkinKeyword {\n const trimmed = keyword.trim();\n // Gherkin keywords may include trailing space, e.g. \"Given \"\n if (trimmed === \"Given\") return \"Given\";\n if (trimmed === \"When\") return \"When\";\n if (trimmed === \"Then\") return \"Then\";\n if (trimmed === \"And\") return \"And\";\n if (trimmed === \"But\") return \"But\";\n // Fallback: treat as Given (shouldn't happen with valid Gherkin)\n return \"Given\";\n}\n\nfunction resolveEffectiveKeywords(\n steps: Omit<ParsedStep, \"effectiveKeyword\">[]\n): ParsedStep[] {\n let lastEffective: \"Given\" | \"When\" | \"Then\" = \"Given\";\n\n return steps.map((step) => {\n let effectiveKeyword: \"Given\" | \"When\" | \"Then\";\n if (step.keyword === \"And\" || step.keyword === \"But\") {\n effectiveKeyword = lastEffective;\n } else {\n effectiveKeyword = step.keyword as \"Given\" | \"When\" | \"Then\";\n }\n lastEffective = effectiveKeyword;\n\n return {\n ...step,\n effectiveKeyword,\n };\n });\n}\n\nfunction substituteExampleValues(\n text: string,\n substitution: Record<string, string>\n): string {\n let result = text;\n for (const [key, value] of Object.entries(substitution)) {\n result = result.replace(new RegExp(`<${key}>`, \"g\"), value);\n }\n return result;\n}\n"],"mappings":"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"}
@@ -1,6 +1,78 @@
1
+ import _ from "lodash";
2
+ import { glob, stat } from "node:fs/promises";
3
+ import * as path from "node:path";
1
4
  import * as fs from "node:fs";
2
5
  import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
3
6
  import * as messages from "@cucumber/messages";
7
+ //#region src/world.ts
8
+ function mergeCustomizer(objValue, srcValue) {
9
+ if (_.isArray(objValue)) return objValue.concat(srcValue);
10
+ else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) throw new Error(`Merge would have destroyed previous value ${objValue} with ${srcValue}`);
11
+ return objValue;
12
+ }
13
+ var BasicWorld = class {
14
+ givenState = {};
15
+ whenState = {};
16
+ thenState = {};
17
+ get given() {
18
+ return {
19
+ ...this.givenState,
20
+ merge: (newState) => {
21
+ this.givenState = _.merge({ ...this.givenState }, newState, mergeCustomizer);
22
+ }
23
+ };
24
+ }
25
+ get when() {
26
+ return {
27
+ ...this.whenState,
28
+ merge: (newState) => {
29
+ this.whenState = _.merge({ ...this.whenState }, newState, mergeCustomizer);
30
+ }
31
+ };
32
+ }
33
+ get then() {
34
+ return {
35
+ ...this.thenState,
36
+ merge: (newState) => {
37
+ this.thenState = _.merge({ ...this.thenState }, newState, mergeCustomizer);
38
+ }
39
+ };
40
+ }
41
+ };
42
+ //#endregion
43
+ //#region src/globFiles.ts
44
+ /** Glob metacharacters. A pattern with none of these is a literal path. */
45
+ const MAGIC = /[*?[\]{}!()]/;
46
+ async function isFile(p) {
47
+ try {
48
+ return (await stat(p)).isFile();
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ /**
54
+ * Resolve glob patterns to a de-duplicated list of absolute file paths, with one
55
+ * important portability guarantee: a **literal absolute path** (no glob magic)
56
+ * is returned directly if it exists, without being handed to `glob()`.
57
+ *
58
+ * This exists because `node:fs`'s `glob` diverges between runtimes — Node
59
+ * matches an absolute-path pattern, Bun returns nothing for one. Rather than
60
+ * depend on that behaviour, we only ever glob relative patterns (against `cwd`)
61
+ * and short-circuit concrete absolute paths ourselves, so callers get identical
62
+ * results under Node and Bun.
63
+ */
64
+ async function globFiles(patterns, cwd = process.cwd()) {
65
+ const files = /* @__PURE__ */ new Set();
66
+ for (const pattern of patterns) {
67
+ if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {
68
+ if (await isFile(pattern)) files.add(pattern);
69
+ continue;
70
+ }
71
+ for await (const match of glob(pattern, { cwd })) files.add(path.resolve(cwd, match));
72
+ }
73
+ return [...files];
74
+ }
75
+ //#endregion
4
76
  //#region src/analyzer/gherkinParser.ts
5
77
  function parseFeatureFiles(filePaths) {
6
78
  const scenarios = [];
@@ -111,6 +183,6 @@ function substituteExampleValues(text, substitution) {
111
183
  return result;
112
184
  }
113
185
  //#endregion
114
- export { parseFeatureFiles as n, parseFeatureContent as t };
186
+ export { BasicWorld as i, parseFeatureFiles as n, globFiles as r, parseFeatureContent as t };
115
187
 
116
- //# sourceMappingURL=gherkinParser-Dp2d7JNr.js.map
188
+ //# sourceMappingURL=gherkinParser-CKARHgt4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gherkinParser-CKARHgt4.js","names":[],"sources":["../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["import _ from \"lodash\";\n\nexport type WorldState<State> = {\n readonly [K in keyof State]?: State[K];\n};\n\nexport type MergeableWorldState<T> = WorldState<T> & {\n merge: (newState: Partial<T>) => void;\n};\n\nfunction mergeCustomizer(objValue: unknown, srcValue: unknown) {\n if (_.isArray(objValue)) {\n return objValue.concat(srcValue);\n } else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) {\n throw new Error(\n `Merge would have destroyed previous value ${objValue} with ${srcValue}`\n );\n }\n return objValue;\n}\n\nexport const createMergeableState = <T>(\n state: WorldState<T>\n): MergeableWorldState<T> => {\n return {\n ...state,\n merge: (newState: Partial<T>) => {\n state = _.merge({ ...state }, newState, mergeCustomizer);\n },\n };\n};\n\nexport type MergeableWorld<Given, When, Then> = {\n given: MergeableWorldState<Given>;\n when: MergeableWorldState<When>;\n then: MergeableWorldState<Then>;\n};\n\nexport class BasicWorld<Given, When, Then> {\n private givenState: WorldState<Given> = {};\n private whenState: WorldState<When> = {};\n private thenState: WorldState<Then> = {};\n\n public get given(): MergeableWorldState<Given> {\n return {\n ...this.givenState,\n merge: (newState: Partial<Given>) => {\n this.givenState = _.merge(\n { ...this.givenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get when(): MergeableWorldState<When> {\n return {\n ...this.whenState,\n merge: (newState: Partial<When>) => {\n this.whenState = _.merge(\n { ...this.whenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n\n public get then(): MergeableWorldState<Then> {\n return {\n ...this.thenState,\n merge: (newState: Partial<Then>) => {\n this.thenState = _.merge(\n { ...this.thenState },\n newState,\n mergeCustomizer\n );\n },\n };\n }\n}\n\nexport const createBasicWorld = <Given, When, Then>(): MergeableWorld<\n Given,\n When,\n Then\n> => {\n return new BasicWorld<Given, When, Then>();\n};\n","import { glob } from \"node:fs/promises\";\nimport { stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\n/** Glob metacharacters. A pattern with none of these is a literal path. */\nconst MAGIC = /[*?[\\]{}!()]/;\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve glob patterns to a de-duplicated list of absolute file paths, with one\n * important portability guarantee: a **literal absolute path** (no glob magic)\n * is returned directly if it exists, without being handed to `glob()`.\n *\n * This exists because `node:fs`'s `glob` diverges between runtimes — Node\n * matches an absolute-path pattern, Bun returns nothing for one. Rather than\n * depend on that behaviour, we only ever glob relative patterns (against `cwd`)\n * and short-circuit concrete absolute paths ourselves, so callers get identical\n * results under Node and Bun.\n */\nexport async function globFiles(\n patterns: string[],\n cwd: string = process.cwd()\n): Promise<string[]> {\n const files = new Set<string>();\n for (const pattern of patterns) {\n if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {\n if (await isFile(pattern)) files.add(pattern);\n continue;\n }\n for await (const match of glob(pattern, { cwd })) {\n files.add(path.resolve(cwd, match));\n }\n }\n return [...files];\n}\n","import * as fs from \"node:fs\";\nimport { GherkinClassicTokenMatcher, Parser, AstBuilder } from \"@cucumber/gherkin\";\nimport * as messages from \"@cucumber/messages\";\nimport { ParsedScenario, ParsedStep } from \"./types.js\";\n\ntype GherkinKeyword = \"Given\" | \"When\" | \"Then\" | \"And\" | \"But\";\n\nexport function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {\n const scenarios: ParsedScenario[] = [];\n\n for (const filePath of filePaths) {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const parsed = parseFeatureContent(content, filePath);\n scenarios.push(...parsed);\n }\n\n return scenarios;\n}\n\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n const newId = messages.IdGenerator.uuid();\n const builder = new AstBuilder(newId);\n const matcher = new GherkinClassicTokenMatcher();\n const parser = new Parser(builder, matcher);\n\n const gherkinDocument: messages.GherkinDocument = parser.parse(content);\n const feature = gherkinDocument.feature;\n if (!feature) return [];\n\n // Collect background steps at the feature level\n const featureBackground: messages.Step[] = [];\n const scenarios: ParsedScenario[] = [];\n const featureTags = tagNames(feature.tags);\n\n for (const child of feature.children) {\n if (child.background) {\n featureBackground.push(...child.background.steps);\n }\n\n if (child.scenario) {\n scenarios.push(\n ...expandScenario(\n child.scenario,\n featureBackground,\n filePath,\n featureTags\n )\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds and tags, both inherited by the\n // rule's scenarios.\n const ruleBackground: messages.Step[] = [...featureBackground];\n const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];\n for (const ruleChild of child.rule.children) {\n if (ruleChild.background) {\n ruleBackground.push(...ruleChild.background.steps);\n }\n if (ruleChild.scenario) {\n scenarios.push(\n ...expandScenario(\n ruleChild.scenario,\n ruleBackground,\n filePath,\n ruleTags\n )\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\n/** Extract tag names (each keeping its leading `@`), deduped in order. */\nfunction tagNames(tags: readonly messages.Tag[] | undefined): string[] {\n return [...new Set((tags ?? []).map(t => t.name))];\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string,\n inheritedTags: string[]\n): ParsedScenario[] {\n const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];\n const hasExamples =\n scenario.examples.length > 0 &&\n scenario.examples.some((e) => e.tableBody.length > 0);\n\n if (!hasExamples) {\n // Regular scenario\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioParsed = convertSteps(scenario.steps);\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);\n\n return [\n {\n name: scenario.name,\n file: filePath,\n steps: allSteps,\n tags: scenarioTags,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row. Rows carry the outline's\n // base name so the runner can group them, plus the Examples-block tags.\n const results: ParsedScenario[] = [];\n for (const example of scenario.examples) {\n if (!example.tableHeader || example.tableBody.length === 0) continue;\n const headers = example.tableHeader.cells.map((c) => c.value);\n const exampleTags = [...scenarioTags, ...tagNames(example.tags)];\n\n for (const row of example.tableBody) {\n const values = row.cells.map((c) => c.value);\n const substitution: Record<string, string> = {};\n headers.forEach((h, i) => {\n substitution[h] = values[i];\n });\n\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioSteps = convertSteps(scenario.steps).map((step) => ({\n ...step,\n text: substituteExampleValues(step.text, substitution),\n }));\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);\n\n results.push({\n name: headers.map((h, i) => `${h}=${values[i]}`).join(\", \"),\n file: filePath,\n steps: allSteps,\n tags: exampleTags,\n outline: { name: scenario.name },\n });\n }\n }\n\n return results;\n}\n\nfunction convertSteps(\n steps: readonly messages.Step[]\n): Omit<ParsedStep, \"effectiveKeyword\">[] {\n return steps.map((step) => ({\n keyword: normalizeKeyword(step.keyword),\n text: step.text,\n line: step.location.line,\n // step.location.column points to the keyword start; shift past the\n // keyword (which includes a trailing space) so column points to the\n // start of the step text. This makes `column + text.length` produce\n // the correct end position for diagnostic ranges.\n column: (step.location.column ?? 1) + step.keyword.length,\n }));\n}\n\nfunction normalizeKeyword(keyword: string): GherkinKeyword {\n const trimmed = keyword.trim();\n // Gherkin keywords may include trailing space, e.g. \"Given \"\n if (trimmed === \"Given\") return \"Given\";\n if (trimmed === \"When\") return \"When\";\n if (trimmed === \"Then\") return \"Then\";\n if (trimmed === \"And\") return \"And\";\n if (trimmed === \"But\") return \"But\";\n // Fallback: treat as Given (shouldn't happen with valid Gherkin)\n return \"Given\";\n}\n\nfunction resolveEffectiveKeywords(\n steps: Omit<ParsedStep, \"effectiveKeyword\">[]\n): ParsedStep[] {\n let lastEffective: \"Given\" | \"When\" | \"Then\" = \"Given\";\n\n return steps.map((step) => {\n let effectiveKeyword: \"Given\" | \"When\" | \"Then\";\n if (step.keyword === \"And\" || step.keyword === \"But\") {\n effectiveKeyword = lastEffective;\n } else {\n effectiveKeyword = step.keyword as \"Given\" | \"When\" | \"Then\";\n }\n lastEffective = effectiveKeyword;\n\n return {\n ...step,\n effectiveKeyword,\n };\n });\n}\n\nfunction substituteExampleValues(\n text: string,\n substitution: Record<string, string>\n): string {\n let result = text;\n for (const [key, value] of Object.entries(substitution)) {\n result = result.replace(new RegExp(`<${key}>`, \"g\"), value);\n }\n return result;\n}\n"],"mappings":"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"}
package/dist/runtime.cjs CHANGED
@@ -1,168 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_hooks = require("./hooks-CGYzwDOv.cjs");
3
- let _cucumber_cucumber_expressions = require("@cucumber/cucumber-expressions");
4
- //#region src/runtime/engine.ts
5
- const keywordToStepType = {
6
- Given: "given",
7
- When: "when",
8
- Then: "then"
9
- };
10
- var UndefinedStepError = class extends Error {
11
- step;
12
- constructor(step) {
13
- super(`Undefined step: ${step.effectiveKeyword} ${step.text}`);
14
- this.step = step;
15
- this.name = "UndefinedStepError";
16
- }
17
- };
18
- var AmbiguousStepError = class extends Error {
19
- step;
20
- matches;
21
- constructor(step, matches) {
22
- super(`Ambiguous step: "${step.text}" matched ${matches.length} definitions:\n` + matches.map((m) => ` - ${m.expression}`).join("\n"));
23
- this.step = step;
24
- this.matches = matches;
25
- this.name = "AmbiguousStepError";
26
- }
27
- };
28
- function compile(registry) {
29
- return registry.all().map((step) => {
30
- const paramRegistry = new _cucumber_cucumber_expressions.ParameterTypeRegistry();
31
- for (const parser of step.parsers) {
32
- if (paramRegistry.lookupByTypeName(parser.name)) continue;
33
- const regexps = Array.isArray(parser.regexp) ? parser.regexp : [parser.regexp];
34
- paramRegistry.defineParameterType(new _cucumber_cucumber_expressions.ParameterType(parser.name, regexps, null, (value) => parser.parse(value)));
35
- }
36
- return {
37
- step,
38
- expression: new _cucumber_cucumber_expressions.CucumberExpression(step.expression, paramRegistry)
39
- };
40
- });
41
- }
42
- /**
43
- * Find the single step definition matching a Gherkin step. Matching is
44
- * opinionated and strict: the keyword must line up with the step type, exactly
45
- * one definition must match, and undefined/ambiguous both throw rather than
46
- * silently skipping (unlike Cucumber's pending/undefined dance).
47
- */
48
- function matchStep(step, compiled) {
49
- const expectedType = keywordToStepType[step.effectiveKeyword];
50
- const matches = [];
51
- for (const { step: def, expression } of compiled) {
52
- if (def.stepType !== expectedType) continue;
53
- const result = expression.match(step.text);
54
- if (result) matches.push({
55
- step: def,
56
- args: result.map((a) => a.getValue(null))
57
- });
58
- }
59
- if (matches.length === 0) throw new UndefinedStepError(step);
60
- if (matches.length > 1) throw new AmbiguousStepError(step, matches.map((m) => m.step));
61
- return matches[0];
62
- }
63
- /**
64
- * Run one scenario against a registry. A fresh world is created per scenario
65
- * (state never leaks between scenarios). On the first failing step the
66
- * remaining steps are marked skipped, matching Cucumber's execution semantics.
67
- *
68
- * Throws on failure so it maps cleanly onto a test runner's `test()` body, but
69
- * the returned/attached `ScenarioResult` carries the per-step breakdown.
70
- */
71
- async function runScenario(scenario, registry, makeWorld, hooks = require_hooks.globalHookRegistry) {
72
- const compiled = compile(registry);
73
- const world = makeWorld();
74
- const scenarioInfo = {
75
- name: scenario.name,
76
- file: scenario.file
77
- };
78
- const steps = [];
79
- let failed = false;
80
- let firstError;
81
- const fail = (err) => {
82
- if (failed) return;
83
- failed = true;
84
- firstError = err instanceof Error ? err : new Error(String(err));
85
- };
86
- try {
87
- for (const hook of hooks.for("scenario", "before")) await hook.fn({
88
- world,
89
- scenario: scenarioInfo
90
- });
91
- } catch (err) {
92
- fail(err);
93
- }
94
- for (const step of scenario.steps) {
95
- if (failed) {
96
- steps.push({
97
- step,
98
- status: "skipped"
99
- });
100
- continue;
101
- }
102
- try {
103
- const { step: def, args } = matchStep(step, compiled);
104
- await def.execute(world, args);
105
- steps.push({
106
- step,
107
- status: "passed"
108
- });
109
- } catch (err) {
110
- const error = err instanceof Error ? err : new Error(String(err));
111
- steps.push({
112
- step,
113
- status: "failed",
114
- error
115
- });
116
- fail(error);
117
- }
118
- }
119
- for (const hook of hooks.for("scenario", "after")) try {
120
- await hook.fn({
121
- world,
122
- scenario: scenarioInfo
123
- });
124
- } catch (err) {
125
- fail(err);
126
- }
127
- const result = {
128
- scenario,
129
- status: failed ? "failed" : "passed",
130
- steps
131
- };
132
- if (failed && firstError) {
133
- const failing = steps.find((s) => s.status === "failed");
134
- if (failing) attachFeatureFrame(firstError, scenario.file, failing.step);
135
- throw firstError;
136
- }
137
- return result;
138
- }
139
- /**
140
- * Prepend a synthetic stack frame pointing at the failing Gherkin step. Because
141
- * the frame's file is the real `.feature` on disk, the test runner treats it as
142
- * a source location and shows a code frame at the step — instead of us jamming
143
- * `file:line` into the error message. The frame goes *above* the real stack, so
144
- * the step-definition frames (the actual throw site) are preserved below it.
145
- */
146
- function attachFeatureFrame(error, file, step) {
147
- const frame = ` at ${`${step.effectiveKeyword} ${step.text}`} (${file}:${step.line}:${step.column})`;
148
- const stack = error.stack;
149
- if (!stack) {
150
- error.stack = `${error.name}: ${error.message}\n${frame}`;
151
- return;
152
- }
153
- const firstFrame = stack.indexOf("\n at ");
154
- if (firstFrame === -1) error.stack = `${stack}\n${frame}`;
155
- else error.stack = stack.slice(0, firstFrame) + `\n${frame}` + stack.slice(firstFrame);
156
- }
157
- //#endregion
158
- exports.AmbiguousStepError = AmbiguousStepError;
3
+ const require_engine = require("./engine-DWAIlwWp.cjs");
4
+ exports.AmbiguousStepError = require_engine.AmbiguousStepError;
159
5
  exports.HookRegistry = require_hooks.HookRegistry;
160
6
  exports.StepRegistry = require_hooks.StepRegistry;
161
- exports.UndefinedStepError = UndefinedStepError;
7
+ exports.UndefinedStepError = require_engine.UndefinedStepError;
8
+ exports.compileRegistry = require_engine.compileRegistry;
162
9
  exports.ensureGlobalHooks = require_hooks.ensureGlobalHooks;
163
10
  exports.globalHookRegistry = require_hooks.globalHookRegistry;
164
11
  exports.globalRegistry = require_hooks.globalRegistry;
165
12
  exports.runHooks = require_hooks.runHooks;
166
- exports.runScenario = runScenario;
167
-
168
- //# sourceMappingURL=runtime.cjs.map
13
+ exports.runScenario = require_engine.runScenario;
@@ -1,4 +1,5 @@
1
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";
2
+ import { CucumberExpression } from "@cucumber/cucumber-expressions";
2
3
 
3
4
  //#region src/runtime/registry.d.ts
4
5
  type StepType = "given" | "when" | "then";
@@ -35,6 +36,11 @@ declare class StepRegistry {
35
36
  declare const globalRegistry: StepRegistry;
36
37
  //#endregion
37
38
  //#region src/runtime/engine.d.ts
39
+ /** A registered step paired with its compiled Cucumber expression. */
40
+ interface CompiledStep {
41
+ step: RegisteredStep;
42
+ expression: CucumberExpression;
43
+ }
38
44
  declare class UndefinedStepError extends Error {
39
45
  readonly step: ParsedStep;
40
46
  constructor(step: ParsedStep);
@@ -44,6 +50,13 @@ declare class AmbiguousStepError extends Error {
44
50
  readonly matches: RegisteredStep[];
45
51
  constructor(step: ParsedStep, matches: RegisteredStep[]);
46
52
  }
53
+ /**
54
+ * Compile a registry's steps into matchable Cucumber expressions **once** per
55
+ * run. The result is reused for every scenario — compilation is pure and depends
56
+ * only on the registry, so recompiling per scenario (as an earlier version did)
57
+ * was wasted work proportional to scenarios × steps.
58
+ */
59
+ declare function compileRegistry(registry: StepRegistry): CompiledStep[];
47
60
  interface StepResult {
48
61
  step: ParsedStep;
49
62
  status: "passed" | "failed" | "skipped";
@@ -54,16 +67,76 @@ interface ScenarioResult {
54
67
  scenario: ParsedScenario;
55
68
  status: "passed" | "failed";
56
69
  steps: StepResult[];
70
+ /**
71
+ * 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.
75
+ */
76
+ error?: Error;
77
+ /** Wall-clock duration of the whole scenario, in milliseconds. */
78
+ durationMs?: number;
57
79
  }
58
80
  /**
59
- * Run one scenario against a registry. A fresh world is created per scenario
60
- * (state never leaks between scenarios). On the first failing step the
61
- * remaining steps are marked skipped, matching Cucumber's execution semantics.
81
+ * Run one scenario against a pre-compiled step table. A fresh world is created
82
+ * per scenario (state never leaks between scenarios). On the first failing step
83
+ * the remaining steps are marked skipped, matching Cucumber's execution
84
+ * semantics.
85
+ *
86
+ * Never throws for step or hook failures: it always resolves to a
87
+ * `ScenarioResult` carrying the per-step breakdown and (on failure) the first
88
+ * `error` with a synthetic `.feature` stack frame attached. Callers decide what
89
+ * to do with a failure — the CLI runner reports it, a test-runner adapter can
90
+ * re-throw `result.error`. It still rejects for truly exceptional conditions
91
+ * (e.g. a bug in the engine itself), never for a normal test failure.
62
92
  *
63
- * Throws on failure so it maps cleanly onto a test runner's `test()` body, but
64
- * the returned/attached `ScenarioResult` carries the per-step breakdown.
93
+ * Pass the compiled table from {@link compileRegistry} once and reuse it across
94
+ * every scenario in the run.
65
95
  */
66
- declare function runScenario(scenario: ParsedScenario, registry: StepRegistry, makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
96
+ declare function runScenario(scenario: ParsedScenario, compiled: CompiledStep[], makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
97
+ //#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
+ }
67
140
  //#endregion
68
- export { AmbiguousStepError, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
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 };
69
142
  //# sourceMappingURL=runtime.d.cts.map
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,5 @@
1
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";
2
+ import { CucumberExpression } from "@cucumber/cucumber-expressions";
2
3
 
3
4
  //#region src/runtime/registry.d.ts
4
5
  type StepType = "given" | "when" | "then";
@@ -35,6 +36,11 @@ declare class StepRegistry {
35
36
  declare const globalRegistry: StepRegistry;
36
37
  //#endregion
37
38
  //#region src/runtime/engine.d.ts
39
+ /** A registered step paired with its compiled Cucumber expression. */
40
+ interface CompiledStep {
41
+ step: RegisteredStep;
42
+ expression: CucumberExpression;
43
+ }
38
44
  declare class UndefinedStepError extends Error {
39
45
  readonly step: ParsedStep;
40
46
  constructor(step: ParsedStep);
@@ -44,6 +50,13 @@ declare class AmbiguousStepError extends Error {
44
50
  readonly matches: RegisteredStep[];
45
51
  constructor(step: ParsedStep, matches: RegisteredStep[]);
46
52
  }
53
+ /**
54
+ * Compile a registry's steps into matchable Cucumber expressions **once** per
55
+ * run. The result is reused for every scenario — compilation is pure and depends
56
+ * only on the registry, so recompiling per scenario (as an earlier version did)
57
+ * was wasted work proportional to scenarios × steps.
58
+ */
59
+ declare function compileRegistry(registry: StepRegistry): CompiledStep[];
47
60
  interface StepResult {
48
61
  step: ParsedStep;
49
62
  status: "passed" | "failed" | "skipped";
@@ -54,16 +67,76 @@ interface ScenarioResult {
54
67
  scenario: ParsedScenario;
55
68
  status: "passed" | "failed";
56
69
  steps: StepResult[];
70
+ /**
71
+ * 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.
75
+ */
76
+ error?: Error;
77
+ /** Wall-clock duration of the whole scenario, in milliseconds. */
78
+ durationMs?: number;
57
79
  }
58
80
  /**
59
- * Run one scenario against a registry. A fresh world is created per scenario
60
- * (state never leaks between scenarios). On the first failing step the
61
- * remaining steps are marked skipped, matching Cucumber's execution semantics.
81
+ * Run one scenario against a pre-compiled step table. A fresh world is created
82
+ * per scenario (state never leaks between scenarios). On the first failing step
83
+ * the remaining steps are marked skipped, matching Cucumber's execution
84
+ * semantics.
85
+ *
86
+ * Never throws for step or hook failures: it always resolves to a
87
+ * `ScenarioResult` carrying the per-step breakdown and (on failure) the first
88
+ * `error` with a synthetic `.feature` stack frame attached. Callers decide what
89
+ * to do with a failure — the CLI runner reports it, a test-runner adapter can
90
+ * re-throw `result.error`. It still rejects for truly exceptional conditions
91
+ * (e.g. a bug in the engine itself), never for a normal test failure.
62
92
  *
63
- * Throws on failure so it maps cleanly onto a test runner's `test()` body, but
64
- * the returned/attached `ScenarioResult` carries the per-step breakdown.
93
+ * Pass the compiled table from {@link compileRegistry} once and reuse it across
94
+ * every scenario in the run.
65
95
  */
66
- declare function runScenario(scenario: ParsedScenario, registry: StepRegistry, makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
96
+ declare function runScenario(scenario: ParsedScenario, compiled: CompiledStep[], makeWorld: () => MergeableWorld<any, any, any>, hooks?: HookRegistry): Promise<ScenarioResult>;
97
+ //#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
+ }
67
140
  //#endregion
68
- export { AmbiguousStepError, HookRegistry, type HookScope, type HookTiming, type PlainHookFn, type RegisteredHook, type RegisteredStep, type ScenarioHookFn, type ScenarioInfo, type ScenarioResult, StepRegistry, type StepResult, type StepType, UndefinedStepError, ensureGlobalHooks, globalHookRegistry, globalRegistry, runHooks, runScenario };
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 };
69
142
  //# sourceMappingURL=runtime.d.ts.map