@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
@@ -0,0 +1,259 @@
1
+ import _ from "lodash";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { glob, stat } from "node:fs/promises";
5
+ import * as fs from "node:fs";
6
+ import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
7
+ import * as messages from "@cucumber/messages";
8
+ //#region src/sourceLocation.ts
9
+ /**
10
+ * Directory holding the library's own compiled code. In development this is the
11
+ * `src/` tree; in the published package it's `dist/` (this module is bundled
12
+ * into the shipped chunks). Stack frames under it are internal plumbing —
13
+ * builders, the engine, the runner — and are hidden from users so that a
14
+ * failure points at *their* step, not ours.
15
+ */
16
+ const LIB_ROOT = path.dirname(fileURLToPath(import.meta.url));
17
+ /**
18
+ * A frame belongs to user code if it's an absolute path that is neither inside
19
+ * the library nor inside any `node_modules` (assertion libs, etc.). That leaves
20
+ * exactly the frames a user cares about: their own step definitions.
21
+ */
22
+ function isUserFile(file) {
23
+ return path.isAbsolute(file) && !file.startsWith(LIB_ROOT + path.sep) && !file.includes(`${path.sep}node_modules${path.sep}`);
24
+ }
25
+ /** Parse one `Error.stack` line into a frame, tolerating V8/Bun variations. */
26
+ function parseFrame(frameLine) {
27
+ const m = /:(\d+):(\d+)\)?\s*$/.exec(frameLine);
28
+ if (!m) return void 0;
29
+ let file = frameLine.slice(0, m.index);
30
+ const paren = file.lastIndexOf("(");
31
+ if (paren !== -1) file = file.slice(paren + 1);
32
+ file = file.trim().replace(/^at\s+/, "");
33
+ if (file.startsWith("file://")) try {
34
+ file = fileURLToPath(file);
35
+ } catch {
36
+ return;
37
+ }
38
+ return {
39
+ file,
40
+ line: Number(m[1]),
41
+ column: Number(m[2])
42
+ };
43
+ }
44
+ /** Every user-code frame in a stack, nearest-first, internals removed. */
45
+ function userFrames(stack) {
46
+ if (!stack) return [];
47
+ const frames = [];
48
+ for (const line of stack.split("\n")) {
49
+ const frame = parseFrame(line);
50
+ if (frame && isUserFile(frame.file)) frames.push(frame);
51
+ }
52
+ return frames;
53
+ }
54
+ /**
55
+ * Capture the user source location of the current call site — the first
56
+ * user-code frame above this function. Called from `.step()` registration so
57
+ * each step remembers where it was defined (Cucumber-style), independent of
58
+ * where an error is later thrown. Returns an absolute `file:line:column`, or
59
+ * `undefined` if no user frame is visible.
60
+ */
61
+ function captureDefinitionSite() {
62
+ const frame = userFrames((/* @__PURE__ */ new Error()).stack)[0];
63
+ return frame ? `${frame.file}:${frame.line}:${frame.column}` : void 0;
64
+ }
65
+ /** Render an absolute `file:line:column` relative to `cwd` for display. */
66
+ function relativeLocation(location, cwd) {
67
+ const m = /^(.*):(\d+):(\d+)$/.exec(location);
68
+ if (!m) return location;
69
+ return `${path.relative(cwd, m[1])}:${m[2]}:${m[3]}`;
70
+ }
71
+ /** Render a frame relative to `cwd`. */
72
+ function relativeFrame(frame, cwd) {
73
+ return `${path.relative(cwd, frame.file)}:${frame.line}:${frame.column}`;
74
+ }
75
+ //#endregion
76
+ //#region src/world.ts
77
+ function mergeCustomizer(objValue, srcValue) {
78
+ if (_.isArray(objValue)) return objValue.concat(srcValue);
79
+ else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) throw new Error(`Merge would have destroyed previous value ${objValue} with ${srcValue}`);
80
+ return objValue;
81
+ }
82
+ var BasicWorld = class {
83
+ givenState = {};
84
+ whenState = {};
85
+ thenState = {};
86
+ get given() {
87
+ return {
88
+ ...this.givenState,
89
+ merge: (newState) => {
90
+ this.givenState = _.merge({ ...this.givenState }, newState, mergeCustomizer);
91
+ }
92
+ };
93
+ }
94
+ get when() {
95
+ return {
96
+ ...this.whenState,
97
+ merge: (newState) => {
98
+ this.whenState = _.merge({ ...this.whenState }, newState, mergeCustomizer);
99
+ }
100
+ };
101
+ }
102
+ get then() {
103
+ return {
104
+ ...this.thenState,
105
+ merge: (newState) => {
106
+ this.thenState = _.merge({ ...this.thenState }, newState, mergeCustomizer);
107
+ }
108
+ };
109
+ }
110
+ };
111
+ //#endregion
112
+ //#region src/globFiles.ts
113
+ /** Glob metacharacters. A pattern with none of these is a literal path. */
114
+ const MAGIC = /[*?[\]{}!()]/;
115
+ async function isFile(p) {
116
+ try {
117
+ return (await stat(p)).isFile();
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+ /**
123
+ * Resolve glob patterns to a de-duplicated list of absolute file paths, with one
124
+ * important portability guarantee: a **literal absolute path** (no glob magic)
125
+ * is returned directly if it exists, without being handed to `glob()`.
126
+ *
127
+ * This exists because `node:fs`'s `glob` diverges between runtimes — Node
128
+ * matches an absolute-path pattern, Bun returns nothing for one. Rather than
129
+ * depend on that behaviour, we only ever glob relative patterns (against `cwd`)
130
+ * and short-circuit concrete absolute paths ourselves, so callers get identical
131
+ * results under Node and Bun.
132
+ */
133
+ async function globFiles(patterns, cwd = process.cwd()) {
134
+ const files = /* @__PURE__ */ new Set();
135
+ for (const pattern of patterns) {
136
+ if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {
137
+ if (await isFile(pattern)) files.add(pattern);
138
+ continue;
139
+ }
140
+ for await (const match of glob(pattern, { cwd })) files.add(path.resolve(cwd, match));
141
+ }
142
+ return [...files];
143
+ }
144
+ //#endregion
145
+ //#region src/analyzer/gherkinParser.ts
146
+ function parseFeatureFiles(filePaths) {
147
+ const scenarios = [];
148
+ for (const filePath of filePaths) {
149
+ const parsed = parseFeatureContent(fs.readFileSync(filePath, "utf-8"), filePath);
150
+ scenarios.push(...parsed);
151
+ }
152
+ return scenarios;
153
+ }
154
+ function parseFeatureContent(content, filePath) {
155
+ const feature = new Parser(new AstBuilder(messages.IdGenerator.uuid()), new GherkinClassicTokenMatcher()).parse(content).feature;
156
+ if (!feature) return [];
157
+ const featureBackground = [];
158
+ const scenarios = [];
159
+ const featureTags = tagNames(feature.tags);
160
+ for (const child of feature.children) {
161
+ if (child.background) featureBackground.push(...child.background.steps);
162
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
163
+ if (child.rule) {
164
+ const ruleBackground = [...featureBackground];
165
+ const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
166
+ for (const ruleChild of child.rule.children) {
167
+ if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
168
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
169
+ }
170
+ }
171
+ }
172
+ return scenarios;
173
+ }
174
+ /** Extract tag names (each keeping its leading `@`), deduped in order. */
175
+ function tagNames(tags) {
176
+ return [...new Set((tags ?? []).map((t) => t.name))];
177
+ }
178
+ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
179
+ const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
180
+ if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
181
+ const bgParsed = convertSteps(backgroundSteps);
182
+ const scenarioParsed = convertSteps(scenario.steps);
183
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
184
+ return [{
185
+ name: scenario.name,
186
+ file: filePath,
187
+ line: scenario.location.line,
188
+ steps: allSteps,
189
+ tags: scenarioTags
190
+ }];
191
+ }
192
+ const results = [];
193
+ for (const example of scenario.examples) {
194
+ if (!example.tableHeader || example.tableBody.length === 0) continue;
195
+ const headers = example.tableHeader.cells.map((c) => c.value);
196
+ const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
197
+ for (const row of example.tableBody) {
198
+ const values = row.cells.map((c) => c.value);
199
+ const substitution = {};
200
+ headers.forEach((h, i) => {
201
+ substitution[h] = values[i];
202
+ });
203
+ const bgParsed = convertSteps(backgroundSteps);
204
+ const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
205
+ ...step,
206
+ text: substituteExampleValues(step.text, substitution)
207
+ }));
208
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
209
+ results.push({
210
+ name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
211
+ file: filePath,
212
+ line: row.location.line,
213
+ steps: allSteps,
214
+ tags: exampleTags,
215
+ outline: { name: scenario.name }
216
+ });
217
+ }
218
+ }
219
+ return results;
220
+ }
221
+ function convertSteps(steps) {
222
+ return steps.map((step) => ({
223
+ keyword: normalizeKeyword(step.keyword),
224
+ text: step.text,
225
+ line: step.location.line,
226
+ column: (step.location.column ?? 1) + step.keyword.length
227
+ }));
228
+ }
229
+ function normalizeKeyword(keyword) {
230
+ const trimmed = keyword.trim();
231
+ if (trimmed === "Given") return "Given";
232
+ if (trimmed === "When") return "When";
233
+ if (trimmed === "Then") return "Then";
234
+ if (trimmed === "And") return "And";
235
+ if (trimmed === "But") return "But";
236
+ return "Given";
237
+ }
238
+ function resolveEffectiveKeywords(steps) {
239
+ let lastEffective = "Given";
240
+ return steps.map((step) => {
241
+ let effectiveKeyword;
242
+ if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
243
+ else effectiveKeyword = step.keyword;
244
+ lastEffective = effectiveKeyword;
245
+ return {
246
+ ...step,
247
+ effectiveKeyword
248
+ };
249
+ });
250
+ }
251
+ function substituteExampleValues(text, substitution) {
252
+ let result = text;
253
+ for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
254
+ return result;
255
+ }
256
+ //#endregion
257
+ export { captureDefinitionSite as a, userFrames as c, BasicWorld as i, parseFeatureFiles as n, relativeFrame as o, globFiles as r, relativeLocation as s, parseFeatureContent as t };
258
+
259
+ //# sourceMappingURL=gherkinParser-NcttZgN4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gherkinParser-NcttZgN4.js","names":[],"sources":["../../src/sourceLocation.ts","../../src/world.ts","../../src/globFiles.ts","../../src/analyzer/gherkinParser.ts"],"sourcesContent":["import * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/**\n * Directory holding the library's own compiled code. In development this is the\n * `src/` tree; in the published package it's `dist/` (this module is bundled\n * into the shipped chunks). Stack frames under it are internal plumbing —\n * builders, the engine, the runner — and are hidden from users so that a\n * failure points at *their* step, not ours.\n */\nconst LIB_ROOT = path.dirname(fileURLToPath(import.meta.url));\n\nexport interface SourceFrame {\n file: string;\n line: number;\n column: number;\n}\n\n/**\n * A frame belongs to user code if it's an absolute path that is neither inside\n * the library nor inside any `node_modules` (assertion libs, etc.). That leaves\n * exactly the frames a user cares about: their own step definitions.\n */\nfunction isUserFile(file: string): boolean {\n return (\n path.isAbsolute(file) &&\n !file.startsWith(LIB_ROOT + path.sep) &&\n !file.includes(`${path.sep}node_modules${path.sep}`)\n );\n}\n\n/** Parse one `Error.stack` line into a frame, tolerating V8/Bun variations. */\nfunction parseFrame(frameLine: string): SourceFrame | undefined {\n const m = /:(\\d+):(\\d+)\\)?\\s*$/.exec(frameLine);\n if (!m) return undefined;\n let file = frameLine.slice(0, m.index);\n const paren = file.lastIndexOf(\"(\");\n if (paren !== -1) file = file.slice(paren + 1);\n file = file.trim().replace(/^at\\s+/, \"\");\n if (file.startsWith(\"file://\")) {\n try {\n file = fileURLToPath(file);\n } catch {\n return undefined;\n }\n }\n return { file, line: Number(m[1]), column: Number(m[2]) };\n}\n\n/** Every user-code frame in a stack, nearest-first, internals removed. */\nexport function userFrames(stack: string | undefined): SourceFrame[] {\n if (!stack) return [];\n const frames: SourceFrame[] = [];\n for (const line of stack.split(\"\\n\")) {\n const frame = parseFrame(line);\n if (frame && isUserFile(frame.file)) frames.push(frame);\n }\n return frames;\n}\n\n/**\n * Capture the user source location of the current call site — the first\n * user-code frame above this function. Called from `.step()` registration so\n * each step remembers where it was defined (Cucumber-style), independent of\n * where an error is later thrown. Returns an absolute `file:line:column`, or\n * `undefined` if no user frame is visible.\n */\nexport function captureDefinitionSite(): string | undefined {\n const frame = userFrames(new Error().stack)[0];\n return frame ? `${frame.file}:${frame.line}:${frame.column}` : undefined;\n}\n\n/** Render an absolute `file:line:column` relative to `cwd` for display. */\nexport function relativeLocation(location: string, cwd: string): string {\n const m = /^(.*):(\\d+):(\\d+)$/.exec(location);\n if (!m) return location;\n return `${path.relative(cwd, m[1])}:${m[2]}:${m[3]}`;\n}\n\n/** Render a frame relative to `cwd`. */\nexport function relativeFrame(frame: SourceFrame, cwd: string): string {\n return `${path.relative(cwd, frame.file)}:${frame.line}:${frame.column}`;\n}\n","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 line: scenario.location.line,\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 line: row.location.line,\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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"}
@@ -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"}