@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
package/README.md CHANGED
@@ -4,6 +4,8 @@ Step Forge is a typed wrapper around the Cucumber library. It provides an opinio
4
4
 
5
5
  This is just a primer, see the [official documentation site](https://step-forge.com) for more information.
6
6
 
7
+ To run your `.feature` files against these steps, see the **[Runtime guide](./RUNTIME.md)** — configuration, the `step-forge` CLI, hooks, filtering, and concurrency.
8
+
7
9
  ## Installation
8
10
 
9
11
  ```bash
package/RUNTIME.md ADDED
@@ -0,0 +1,242 @@
1
+ # Step Forge Runtime
2
+
3
+ The Step Forge runtime executes your `.feature` files against your step
4
+ definitions. It is a native TypeScript runner built for speed and Cucumber-like
5
+ semantics — it does **not** run the Cucumber.js runtime, and it does not require
6
+ a bundler or a separate test framework.
7
+
8
+ If you only want to run features, this document is all you need. For how to
9
+ _write_ steps, the world, parsers, and dependencies, see the main
10
+ [README](./README.md).
11
+
12
+ ---
13
+
14
+ ## Requirements
15
+
16
+ The runner executes under **[Bun](https://bun.sh)** so your TypeScript step
17
+ files run directly, with no transpile/build step and no loader configuration.
18
+ Bun is the one hard requirement:
19
+
20
+ ```bash
21
+ curl -fsSL https://bun.sh/install | bash # or: brew install oven-sh/bun/bun
22
+ ```
23
+
24
+ Everything the runner touches beyond that uses standard `node:` APIs, so your
25
+ step code stays ordinary TypeScript.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install --save-dev @step-forge/step-forge
31
+ ```
32
+
33
+ ## Quickstart
34
+
35
+ 1. **Write step definitions.** A step self-registers the moment you call
36
+ `.step(...)` — no separate registration call. Put them in files matched by
37
+ your `steps` glob (default `**/*.steps.ts`).
38
+
39
+ ```ts
40
+ // features/steps/user.steps.ts
41
+ import { givenBuilder, thenBuilder } from "@step-forge/step-forge";
42
+
43
+ givenBuilder<{ user: string }>()
44
+ .statement((name: string) => `a user named ${name}`)
45
+ .step(({ variables: [name] }) => ({ user: name }));
46
+
47
+ thenBuilder<{ user: string }, {}, {}>()
48
+ .statement((name: string) => `the user is ${name}`)
49
+ .dependencies({ given: { user: "required" } })
50
+ .step(({ variables: [name], given: { user } }) => {
51
+ if (user !== name) throw new Error(`expected ${name}, got ${user}`);
52
+ });
53
+ ```
54
+
55
+ 2. **Provide a world factory** (optional). One fresh world is created per
56
+ scenario, so state never leaks between scenarios. Omit this to get a default
57
+ `BasicWorld`.
58
+
59
+ ```ts
60
+ // features/support/world.ts
61
+ import { BasicWorld } from "@step-forge/step-forge";
62
+ export default () => new BasicWorld();
63
+ ```
64
+
65
+ 3. **Add a config file** (optional but recommended).
66
+
67
+ ```ts
68
+ // step-forge.config.ts
69
+ import type { RunnerOptions } from "@step-forge/step-forge";
70
+
71
+ const config: RunnerOptions = {
72
+ features: "features/**/*.feature",
73
+ steps: "features/**/*.steps.ts",
74
+ world: "features/support/world.ts",
75
+ };
76
+ export default config;
77
+ ```
78
+
79
+ 4. **Run.**
80
+
81
+ ```bash
82
+ step-forge
83
+ ```
84
+
85
+ A typical `package.json` wires it as the test script:
86
+
87
+ ```json
88
+ { "scripts": { "test": "step-forge" } }
89
+ ```
90
+
91
+ ## Configuration
92
+
93
+ Config is resolved from three sources, later ones overriding earlier ones:
94
+ **defaults → `step-forge.config.ts` → CLI flags**. Every field is optional.
95
+
96
+ | Field | Type | Default | Meaning |
97
+ | ------------- | ------------------------- | ------------------ | ----------------------------------------------------------------------- |
98
+ | `features` | `string \| string[]` | `**/*.feature` | Feature-file glob(s), relative to the config directory. |
99
+ | `steps` | `string \| string[]` | `**/*.steps.ts` | Step-module glob(s). Importing them is what registers your steps. |
100
+ | `world` | `string` | `BasicWorld` | Module that default-exports a world factory `() => world`. |
101
+ | `concurrency` | `number` | `1` (serial) | Max scenarios in flight at once. See [Concurrency](#concurrency). |
102
+ | `reporter` | `"pretty" \| "progress"` | `pretty` | Output style. See [Reporters & output](#reporters--output). |
103
+ | `verbose` | `boolean` | `false` | Report every scenario, not just failures. |
104
+ | `name` | `string` | — | Only scenarios whose name matches (substring, or `/regex/flags`). |
105
+ | `tags` | `string` | — | Cucumber tag expression, e.g. `@smoke and not @wip`. |
106
+
107
+ The config file is loaded by Bun, so it may be TypeScript and import the
108
+ `RunnerOptions` type for editor help.
109
+
110
+ ## CLI
111
+
112
+ ```
113
+ step-forge [options] [feature globs...]
114
+ ```
115
+
116
+ Positional arguments are feature globs and **override** the configured
117
+ `features`, so you can run a subset ad hoc.
118
+
119
+ | Flag | Short | Description |
120
+ | ----------------------- | ----- | ------------------------------------------------------- |
121
+ | `--tags <expr>` | `-t` | Tag expression, e.g. `"@smoke and not @wip"`. |
122
+ | `--name <pattern>` | `-n` | Only scenarios whose name matches (substring/`/regex/`).|
123
+ | `--steps <glob>` | `-s` | Step-module glob (repeatable). |
124
+ | `--world <module>` | `-w` | World factory module. |
125
+ | `--concurrency <n>` | `-c` | Max scenarios in flight (default `1`). |
126
+ | `--reporter <name>` | `-r` | `pretty` (default) or `progress`. |
127
+ | `--verbose` | `-v` | Report every scenario, not just failures. |
128
+ | `--config <path>` | | Directory to resolve the config file and globs from. |
129
+ | `--help` | `-h` | Show usage. |
130
+
131
+ Examples:
132
+
133
+ ```bash
134
+ step-forge features/checkout.feature # one file
135
+ step-forge --tags "@smoke and not @wip" # by tag
136
+ step-forge --name "logs in" # by scenario name
137
+ step-forge --concurrency 8 --reporter progress # parallel, compact output
138
+ ```
139
+
140
+ The process exits `0` when every scenario passes and `1` when any scenario
141
+ fails, so it drops straight into CI.
142
+
143
+ ## Hooks
144
+
145
+ Hooks are side-effect callbacks for setup/teardown (start a server, reset a
146
+ mock). They **never seed scenario state** — that flows exclusively through
147
+ `given`/`when`/`then` so the typed dependency graph stays the single source of
148
+ truth. Import them from the main entry:
149
+
150
+ ```ts
151
+ import {
152
+ beforeAll,
153
+ afterAll,
154
+ beforeFeature,
155
+ afterFeature,
156
+ beforeScenario,
157
+ afterScenario,
158
+ } from "@step-forge/step-forge";
159
+
160
+ beforeScenario(({ world, scenario }) => {
161
+ /* fresh world available; reset external state here */
162
+ });
163
+ ```
164
+
165
+ | Hook | Runs |
166
+ | --------------------- | ------------------------------------------------ |
167
+ | `beforeAll`/`afterAll` | Once around the entire run — `beforeAll` before concurrency starts, `afterAll` after every scenario is done. |
168
+ | `beforeFeature`/`afterFeature` | Around each feature file. |
169
+ | `beforeScenario`/`afterScenario` | Around each scenario (gets its world).|
170
+
171
+ Multiple `beforeAll` (and multiple `afterAll`) hooks run **in parallel** with no
172
+ ordering between them — if one setup step must precede another, sequence both
173
+ inside a single hook. Feature and scenario `after*` hooks instead run in reverse
174
+ registration order so teardown unwinds setup, and `afterScenario` runs even when
175
+ a step failed.
176
+
177
+ ## Filtering
178
+
179
+ - **Tags** — `--tags` / `tags` accepts a Cucumber tag expression with `and`,
180
+ `or`, `not`, parentheses, and `@tag` atoms:
181
+ `@smoke and (@fast or not @flaky)`.
182
+ - **Name** — `--name` / `name` is a substring by default, or a regex if written
183
+ as `/pattern/flags`.
184
+ - **`@skip`** — a scenario tagged `@skip` is never executed and is reported as
185
+ skipped (not dropped silently).
186
+ - **`@only`** — if any selected scenario is tagged `@only`, the run is focused to
187
+ just those. `@skip` wins over `@only`.
188
+
189
+ ## Concurrency
190
+
191
+ Scenarios run **serially by default** (`concurrency: 1`), matching Cucumber.
192
+ Raise `--concurrency` (or the `concurrency` config field) to run scenarios
193
+ concurrently in a single process — ideal for I/O-bound suites.
194
+
195
+ This is safe because of one opinionated invariant Step Forge holds:
196
+
197
+ > **Module state is immutable and scenario-isolated.** All mutable scenario
198
+ > state lives in the per-scenario world (already isolated); anything at module
199
+ > scope is write-once configuration or pure lookups.
200
+
201
+ Because mutable state can't be shared across scenarios, concurrent scenarios
202
+ cannot corrupt one another — no worker threads or process isolation required.
203
+ The corollary is a rule to follow in your own code: **do not have a hook or step
204
+ write to a mutable module-level variable that another step reads.** That is a
205
+ cross-scenario data race and the only way to break parallel runs. Keep
206
+ per-scenario state in the world and you can turn concurrency up freely.
207
+
208
+ ## Reporters & output
209
+
210
+ By default the runner is quiet: it prints a dot per scenario as a heartbeat
211
+ (`.` pass, `F` fail, `-` skip), then **only the failing scenarios** in detail,
212
+ then a summary. Pass `--verbose` (`-v`) to report every scenario instead.
213
+
214
+ Each failure is shown Cucumber-style so it's easy to locate:
215
+
216
+ ```
217
+ ✗ a user can log in
218
+ features/auth.feature:12
219
+
220
+ ✓ Given a registered user
221
+ ✗ Then they reach the dashboard
222
+ feature: features/auth.feature:15
223
+ defined: features/steps/auth.steps.ts:40
224
+ AssertionError: expected "/login" to equal "/dashboard"
225
+ at features/steps/auth.steps.ts:42:18
226
+ ```
227
+
228
+ - **`feature:`** — the `.feature` file and line of the failing step.
229
+ - **`defined:`** — where that step is defined (its `.step(...)` call site).
230
+ - The stack is trimmed to your own code (library, engine, and `node_modules`
231
+ frames removed) and source-mapped by Bun to the original TypeScript, so the
232
+ top frame is the line in your step that actually threw.
233
+
234
+ Two styles are available via `--reporter`:
235
+
236
+ - **`pretty`** (default) — honours `--verbose`: failures-only by default, or the
237
+ full feature → scenario → step tree (each step annotated with its definition
238
+ location) under `--verbose`.
239
+ - **`progress`** — always compact (dots + failures + summary); ignores
240
+ `--verbose`.
241
+
242
+ Colour is emitted only to a TTY and is disabled when `NO_COLOR` is set.
@@ -1,7 +1,42 @@
1
- import { n as parseFeatureFiles } from "./gherkinParser-Dp2d7JNr.js";
2
- import { glob } from "node:fs/promises";
1
+ import { glob, stat } from "node:fs/promises";
3
2
  import * as path from "node:path";
4
3
  import ts from "typescript";
4
+ import * as fs from "node:fs";
5
+ import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
6
+ import * as messages from "@cucumber/messages";
7
+ //#region src/globFiles.ts
8
+ /** Glob metacharacters. A pattern with none of these is a literal path. */
9
+ const MAGIC = /[*?[\]{}!()]/;
10
+ async function isFile(p) {
11
+ try {
12
+ return (await stat(p)).isFile();
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+ /**
18
+ * Resolve glob patterns to a de-duplicated list of absolute file paths, with one
19
+ * important portability guarantee: a **literal absolute path** (no glob magic)
20
+ * is returned directly if it exists, without being handed to `glob()`.
21
+ *
22
+ * This exists because `node:fs`'s `glob` diverges between runtimes — Node
23
+ * matches an absolute-path pattern, Bun returns nothing for one. Rather than
24
+ * depend on that behaviour, we only ever glob relative patterns (against `cwd`)
25
+ * and short-circuit concrete absolute paths ourselves, so callers get identical
26
+ * results under Node and Bun.
27
+ */
28
+ async function globFiles(patterns, cwd = process.cwd()) {
29
+ const files = /* @__PURE__ */ new Set();
30
+ for (const pattern of patterns) {
31
+ if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {
32
+ if (await isFile(pattern)) files.add(pattern);
33
+ continue;
34
+ }
35
+ for await (const match of glob(pattern, { cwd })) files.add(path.resolve(cwd, match));
36
+ }
37
+ return [...files];
38
+ }
39
+ //#endregion
5
40
  //#region src/analyzer/stepExtractor.ts
6
41
  const BUILDER_NAMES = {
7
42
  givenBuilder: "given",
@@ -9,6 +44,21 @@ const BUILDER_NAMES = {
9
44
  thenBuilder: "then"
10
45
  };
11
46
  function extractStepDefinitions(filePaths, tsConfigPath) {
47
+ const fast = extractWithSources(filePaths.map(parseSourceFile).filter((s) => s !== null), null);
48
+ if (!fast.needsChecker) return fast.results;
49
+ const program = ts.createProgram(filePaths, resolveCompilerOptions(tsConfigPath));
50
+ const checker = program.getTypeChecker();
51
+ return extractWithSources(filePaths.map((fp) => program.getSourceFile(fp)).filter((s) => s !== void 0), checker).results;
52
+ }
53
+ /** Parse a single file into an AST with no type resolution. */
54
+ function parseSourceFile(filePath) {
55
+ const text = ts.sys.readFile(filePath);
56
+ if (text === void 0) return null;
57
+ const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
58
+ return ts.createSourceFile(filePath, text, ts.ScriptTarget.ESNext, true, scriptKind);
59
+ }
60
+ /** Resolve compiler options from tsconfig (fallback to sane defaults). */
61
+ function resolveCompilerOptions(tsConfigPath) {
12
62
  const configPath = tsConfigPath ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);
13
63
  let compilerOptions = {
14
64
  target: ts.ScriptTarget.ESNext,
@@ -22,22 +72,25 @@ function extractStepDefinitions(filePaths, tsConfigPath) {
22
72
  if (!configFile.error) compilerOptions = ts.parseJsonConfigFileContent(configFile.config, ts.sys, configPath.replace(/[/\\][^/\\]+$/, "")).options;
23
73
  }
24
74
  compilerOptions.noEmit = true;
25
- const program = ts.createProgram(filePaths, compilerOptions);
26
- const checker = program.getTypeChecker();
75
+ return compilerOptions;
76
+ }
77
+ function extractWithSources(sources, checker) {
78
+ const ctx = {
79
+ checker,
80
+ needsChecker: false
81
+ };
27
82
  const results = [];
28
- for (const filePath of filePaths) {
29
- const sourceFile = program.getSourceFile(filePath);
30
- if (!sourceFile) continue;
31
- const fileResults = extractFromSourceFile(sourceFile, checker);
32
- results.push(...fileResults);
33
- }
34
- return results;
83
+ for (const sourceFile of sources) results.push(...extractFromSourceFile(sourceFile, ctx));
84
+ return {
85
+ results,
86
+ needsChecker: ctx.needsChecker
87
+ };
35
88
  }
36
- function extractFromSourceFile(sourceFile, checker) {
89
+ function extractFromSourceFile(sourceFile, ctx) {
37
90
  const results = [];
38
91
  function visit(node) {
39
92
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
40
- const meta = extractFromRegisterCall(node, sourceFile, checker);
93
+ const meta = extractFromRegisterCall(node, sourceFile, ctx);
41
94
  if (meta) results.push(meta);
42
95
  }
43
96
  ts.forEachChild(node, visit);
@@ -45,7 +98,7 @@ function extractFromSourceFile(sourceFile, checker) {
45
98
  visit(sourceFile);
46
99
  return results;
47
100
  }
48
- function extractFromRegisterCall(registerCall, sourceFile, checker) {
101
+ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
49
102
  const chain = collectCallChain(registerCall);
50
103
  let stepType = null;
51
104
  let expression = null;
@@ -60,7 +113,7 @@ function extractFromRegisterCall(registerCall, sourceFile, checker) {
60
113
  if (!name) continue;
61
114
  if (name === "register") continue;
62
115
  if (name === "step") {
63
- produces = extractProducedKeys(link, checker);
116
+ produces = extractProducedKeys(link, ctx);
64
117
  continue;
65
118
  }
66
119
  if (name === "dependencies") {
@@ -77,7 +130,7 @@ function extractFromRegisterCall(registerCall, sourceFile, checker) {
77
130
  }
78
131
  }
79
132
  if (!stepType || !expression) {
80
- const reExport = resolveReExportedCall(chain, checker);
133
+ const reExport = resolveReExportedCall(chain, ctx);
81
134
  if (reExport) {
82
135
  if (!stepType) stepType = reExport.stepType;
83
136
  if (!expression) expression = reExport.expression;
@@ -164,33 +217,37 @@ function extractDependencies(depsCall) {
164
217
  }
165
218
  return deps;
166
219
  }
167
- function extractProducedKeys(stepCall, checker) {
220
+ function extractProducedKeys(stepCall, ctx) {
168
221
  const callback = stepCall.arguments[0];
169
222
  if (!callback) return [];
170
- if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, checker);
223
+ if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, ctx);
171
224
  return [];
172
225
  }
173
- function extractProducedKeysFromCallback(callback, checker) {
226
+ function extractProducedKeysFromCallback(callback, ctx) {
174
227
  const body = callback.body;
175
- if (!ts.isBlock(body)) return extractKeysFromExpression(body, checker);
228
+ if (!ts.isBlock(body)) return extractKeysFromExpression(body, ctx);
176
229
  const keys = /* @__PURE__ */ new Set();
177
230
  function visitReturn(node) {
178
- if (ts.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, checker)) keys.add(key);
231
+ if (ts.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, ctx)) keys.add(key);
179
232
  ts.forEachChild(node, visitReturn);
180
233
  }
181
234
  visitReturn(body);
182
235
  return [...keys];
183
236
  }
184
- function extractKeysFromExpression(expr, checker) {
237
+ function extractKeysFromExpression(expr, ctx) {
185
238
  while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
186
239
  if (ts.isObjectLiteralExpression(expr)) return expr.properties.filter((p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)).map((p) => p.name.getText()).filter(Boolean);
240
+ if (!ctx.checker) {
241
+ ctx.needsChecker = true;
242
+ return [];
243
+ }
187
244
  try {
188
- return checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
245
+ return ctx.checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
189
246
  } catch {
190
247
  return [];
191
248
  }
192
249
  }
193
- function resolveReExportedCall(chain, checker) {
250
+ function resolveReExportedCall(chain, ctx) {
194
251
  const lastCall = chain[chain.length - 1];
195
252
  if (!lastCall) return null;
196
253
  const expr = lastCall.expression;
@@ -198,7 +255,11 @@ function resolveReExportedCall(chain, checker) {
198
255
  if (ts.isIdentifier(expr)) identifier = expr;
199
256
  else if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) identifier = expr.expression;
200
257
  if (!identifier) return null;
201
- const symbol = checker.getSymbolAtLocation(identifier);
258
+ if (!ctx.checker) {
259
+ ctx.needsChecker = true;
260
+ return null;
261
+ }
262
+ const symbol = ctx.checker.getSymbolAtLocation(identifier);
202
263
  if (!symbol) return null;
203
264
  const decl = symbol.valueDeclaration;
204
265
  if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return null;
@@ -219,6 +280,118 @@ function resolveReExportedCall(chain, checker) {
219
280
  return null;
220
281
  }
221
282
  //#endregion
283
+ //#region src/analyzer/gherkinParser.ts
284
+ function parseFeatureFiles(filePaths) {
285
+ const scenarios = [];
286
+ for (const filePath of filePaths) {
287
+ const parsed = parseFeatureContent(fs.readFileSync(filePath, "utf-8"), filePath);
288
+ scenarios.push(...parsed);
289
+ }
290
+ return scenarios;
291
+ }
292
+ function parseFeatureContent(content, filePath) {
293
+ const feature = new Parser(new AstBuilder(messages.IdGenerator.uuid()), new GherkinClassicTokenMatcher()).parse(content).feature;
294
+ if (!feature) return [];
295
+ const featureBackground = [];
296
+ const scenarios = [];
297
+ const featureTags = tagNames(feature.tags);
298
+ for (const child of feature.children) {
299
+ if (child.background) featureBackground.push(...child.background.steps);
300
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
301
+ if (child.rule) {
302
+ const ruleBackground = [...featureBackground];
303
+ const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
304
+ for (const ruleChild of child.rule.children) {
305
+ if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
306
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
307
+ }
308
+ }
309
+ }
310
+ return scenarios;
311
+ }
312
+ /** Extract tag names (each keeping its leading `@`), deduped in order. */
313
+ function tagNames(tags) {
314
+ return [...new Set((tags ?? []).map((t) => t.name))];
315
+ }
316
+ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
317
+ const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
318
+ if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
319
+ const bgParsed = convertSteps(backgroundSteps);
320
+ const scenarioParsed = convertSteps(scenario.steps);
321
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
322
+ return [{
323
+ name: scenario.name,
324
+ file: filePath,
325
+ line: scenario.location.line,
326
+ steps: allSteps,
327
+ tags: scenarioTags
328
+ }];
329
+ }
330
+ const results = [];
331
+ for (const example of scenario.examples) {
332
+ if (!example.tableHeader || example.tableBody.length === 0) continue;
333
+ const headers = example.tableHeader.cells.map((c) => c.value);
334
+ const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
335
+ for (const row of example.tableBody) {
336
+ const values = row.cells.map((c) => c.value);
337
+ const substitution = {};
338
+ headers.forEach((h, i) => {
339
+ substitution[h] = values[i];
340
+ });
341
+ const bgParsed = convertSteps(backgroundSteps);
342
+ const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
343
+ ...step,
344
+ text: substituteExampleValues(step.text, substitution)
345
+ }));
346
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
347
+ results.push({
348
+ name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
349
+ file: filePath,
350
+ line: row.location.line,
351
+ steps: allSteps,
352
+ tags: exampleTags,
353
+ outline: { name: scenario.name }
354
+ });
355
+ }
356
+ }
357
+ return results;
358
+ }
359
+ function convertSteps(steps) {
360
+ return steps.map((step) => ({
361
+ keyword: normalizeKeyword(step.keyword),
362
+ text: step.text,
363
+ line: step.location.line,
364
+ column: (step.location.column ?? 1) + step.keyword.length
365
+ }));
366
+ }
367
+ function normalizeKeyword(keyword) {
368
+ const trimmed = keyword.trim();
369
+ if (trimmed === "Given") return "Given";
370
+ if (trimmed === "When") return "When";
371
+ if (trimmed === "Then") return "Then";
372
+ if (trimmed === "And") return "And";
373
+ if (trimmed === "But") return "But";
374
+ return "Given";
375
+ }
376
+ function resolveEffectiveKeywords(steps) {
377
+ let lastEffective = "Given";
378
+ return steps.map((step) => {
379
+ let effectiveKeyword;
380
+ if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
381
+ else effectiveKeyword = step.keyword;
382
+ lastEffective = effectiveKeyword;
383
+ return {
384
+ ...step,
385
+ effectiveKeyword
386
+ };
387
+ });
388
+ }
389
+ function substituteExampleValues(text, substitution) {
390
+ let result = text;
391
+ for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
392
+ return result;
393
+ }
394
+ //#endregion
222
395
  //#region src/analyzer/stepMatcher.ts
223
396
  function compileDefinitions(definitions) {
224
397
  const compiled = [];
@@ -353,8 +526,8 @@ function runRules(rules, scenario, matchedSteps) {
353
526
  //#region src/analyzer/index.ts
354
527
  async function analyze(config, options) {
355
528
  const rules = options?.rules ?? defaultRules;
356
- const stepFilePaths = await resolveGlobs(config.stepFiles);
357
- const featureFilePaths = await resolveGlobs(config.featureFiles);
529
+ const stepFilePaths = await globFiles(config.stepFiles);
530
+ const featureFilePaths = await globFiles(config.featureFiles);
358
531
  if (stepFilePaths.length === 0) return [];
359
532
  if (featureFilePaths.length === 0) return [];
360
533
  const stepDefinitions = extractStepDefinitions(stepFilePaths, config.tsConfigPath);
@@ -366,12 +539,7 @@ async function analyze(config, options) {
366
539
  }
367
540
  return diagnostics;
368
541
  }
369
- async function resolveGlobs(patterns) {
370
- const files = [];
371
- for (const pattern of patterns) for await (const file of glob(pattern)) files.push(path.resolve(file));
372
- return [...new Set(files)];
373
- }
374
542
  //#endregion
375
- export { extractStepDefinitions as a, matchScenarioSteps as i, defaultRules as n, findMatchingDefinitions as r, analyze as t };
543
+ export { parseFeatureContent as a, matchScenarioSteps as i, defaultRules as n, parseFeatureFiles as o, findMatchingDefinitions as r, extractStepDefinitions as s, analyze as t };
376
544
 
377
- //# sourceMappingURL=analyzer-DJyJbU_V.js.map
545
+ //# sourceMappingURL=analyzer-byS8yRrY.js.map