@step-forge/step-forge 0.0.17 → 0.0.20

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.
package/README.md CHANGED
@@ -16,7 +16,6 @@ npm install @step-forge/step-forge
16
16
 
17
17
  ```ts
18
18
  // features/steps/world.ts
19
- import { setWorldConstructor } from "@cucumber/cucumber";
20
19
  import { BasicWorld } from "@step-forge/step-forge";
21
20
 
22
21
  export interface GivenState {
@@ -29,7 +28,9 @@ export interface WhenState {
29
28
 
30
29
  export interface ThenState {}
31
30
 
32
- setWorldConstructor(BasicWorld<GivenState, WhenState, ThenState>);
31
+ // The native runtime creates one fresh world per scenario. With the Vitest
32
+ // plugin, point `world` at a module that default-exports this factory.
33
+ export default () => new BasicWorld<GivenState, WhenState, ThenState>();
33
34
  ```
34
35
 
35
36
  ### Defining Steps
@@ -1,9 +1,7 @@
1
+ import { n as parseFeatureFiles } from "./gherkinParser-Dp2d7JNr.js";
1
2
  import { glob } from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  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
5
  //#region src/analyzer/stepExtractor.ts
8
6
  const BUILDER_NAMES = {
9
7
  givenBuilder: "given",
@@ -38,7 +36,7 @@ function extractStepDefinitions(filePaths, tsConfigPath) {
38
36
  function extractFromSourceFile(sourceFile, checker) {
39
37
  const results = [];
40
38
  function visit(node) {
41
- if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "register") {
39
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
42
40
  const meta = extractFromRegisterCall(node, sourceFile, checker);
43
41
  if (meta) results.push(meta);
44
42
  }
@@ -221,105 +219,6 @@ function resolveReExportedCall(chain, checker) {
221
219
  return null;
222
220
  }
223
221
  //#endregion
224
- //#region src/analyzer/gherkinParser.ts
225
- function parseFeatureFiles(filePaths) {
226
- const scenarios = [];
227
- for (const filePath of filePaths) {
228
- const parsed = parseFeatureContent(fs.readFileSync(filePath, "utf-8"), filePath);
229
- scenarios.push(...parsed);
230
- }
231
- return scenarios;
232
- }
233
- function parseFeatureContent(content, filePath) {
234
- const feature = new Parser(new AstBuilder(messages.IdGenerator.uuid()), new GherkinClassicTokenMatcher()).parse(content).feature;
235
- if (!feature) return [];
236
- const featureBackground = [];
237
- const scenarios = [];
238
- for (const child of feature.children) {
239
- if (child.background) featureBackground.push(...child.background.steps);
240
- if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath));
241
- if (child.rule) {
242
- const ruleBackground = [...featureBackground];
243
- for (const ruleChild of child.rule.children) {
244
- if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
245
- if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath));
246
- }
247
- }
248
- }
249
- return scenarios;
250
- }
251
- function expandScenario(scenario, backgroundSteps, filePath) {
252
- if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
253
- const bgParsed = convertSteps(backgroundSteps);
254
- const scenarioParsed = convertSteps(scenario.steps);
255
- const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
256
- return [{
257
- name: scenario.name,
258
- file: filePath,
259
- steps: allSteps
260
- }];
261
- }
262
- const results = [];
263
- for (const example of scenario.examples) {
264
- if (!example.tableHeader || example.tableBody.length === 0) continue;
265
- const headers = example.tableHeader.cells.map((c) => c.value);
266
- for (const row of example.tableBody) {
267
- const values = row.cells.map((c) => c.value);
268
- const substitution = {};
269
- headers.forEach((h, i) => {
270
- substitution[h] = values[i];
271
- });
272
- const bgParsed = convertSteps(backgroundSteps);
273
- const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
274
- ...step,
275
- text: substituteExampleValues(step.text, substitution)
276
- }));
277
- const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
278
- results.push({
279
- name: `${scenario.name} (${values.join(", ")})`,
280
- file: filePath,
281
- steps: allSteps
282
- });
283
- }
284
- }
285
- return results;
286
- }
287
- function convertSteps(steps) {
288
- return steps.map((step) => ({
289
- keyword: normalizeKeyword(step.keyword),
290
- text: step.text,
291
- line: step.location.line,
292
- column: (step.location.column ?? 1) + step.keyword.length
293
- }));
294
- }
295
- function normalizeKeyword(keyword) {
296
- const trimmed = keyword.trim();
297
- if (trimmed === "Given") return "Given";
298
- if (trimmed === "When") return "When";
299
- if (trimmed === "Then") return "Then";
300
- if (trimmed === "And") return "And";
301
- if (trimmed === "But") return "But";
302
- return "Given";
303
- }
304
- function resolveEffectiveKeywords(steps) {
305
- let lastEffective = "Given";
306
- return steps.map((step) => {
307
- let effectiveKeyword;
308
- if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
309
- else effectiveKeyword = step.keyword;
310
- lastEffective = effectiveKeyword;
311
- return {
312
- ...step,
313
- effectiveKeyword
314
- };
315
- });
316
- }
317
- function substituteExampleValues(text, substitution) {
318
- let result = text;
319
- for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
320
- return result;
321
- }
322
- //#endregion
323
222
  //#region src/analyzer/stepMatcher.ts
324
223
  function compileDefinitions(definitions) {
325
224
  const compiled = [];
@@ -473,6 +372,6 @@ async function resolveGlobs(patterns) {
473
372
  return [...new Set(files)];
474
373
  }
475
374
  //#endregion
476
- export { parseFeatureContent as a, matchScenarioSteps as i, defaultRules as n, parseFeatureFiles as o, findMatchingDefinitions as r, extractStepDefinitions as s, analyze as t };
375
+ export { extractStepDefinitions as a, matchScenarioSteps as i, defaultRules as n, findMatchingDefinitions as r, analyze as t };
477
376
 
478
- //# sourceMappingURL=analyzer-QH03uejK.js.map
377
+ //# sourceMappingURL=analyzer-DJyJbU_V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer-DJyJbU_V.js","names":[],"sources":["../../src/analyzer/stepExtractor.ts","../../src/analyzer/stepMatcher.ts","../../src/analyzer/rules/ambiguousStepRule.ts","../../src/analyzer/rules/dependencyRule.ts","../../src/analyzer/rules/undefinedStepRule.ts","../../src/analyzer/rules/index.ts","../../src/analyzer/index.ts"],"sourcesContent":["import ts from \"typescript\";\nimport { StepDefinitionMeta } from \"./types.js\";\n\ntype StepType = \"given\" | \"when\" | \"then\";\n\nconst BUILDER_NAMES: Record<string, StepType> = {\n givenBuilder: \"given\",\n whenBuilder: \"when\",\n thenBuilder: \"then\",\n};\n\nexport function extractStepDefinitions(\n filePaths: string[],\n tsConfigPath?: string\n): StepDefinitionMeta[] {\n const configPath =\n tsConfigPath ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);\n let compilerOptions: ts.CompilerOptions = {\n target: ts.ScriptTarget.ESNext,\n module: ts.ModuleKind.ESNext,\n moduleResolution: ts.ModuleResolutionKind.Node10,\n strict: true,\n esModuleInterop: true,\n };\n\n if (configPath) {\n const configFile = ts.readConfigFile(configPath, ts.sys.readFile);\n if (!configFile.error) {\n const parsed = ts.parseJsonConfigFileContent(\n configFile.config,\n ts.sys,\n configPath.replace(/[/\\\\][^/\\\\]+$/, \"\")\n );\n compilerOptions = parsed.options;\n }\n }\n\n // Ensure noEmit so we don't write files\n compilerOptions.noEmit = true;\n\n const program = ts.createProgram(filePaths, compilerOptions);\n const checker = program.getTypeChecker();\n const results: StepDefinitionMeta[] = [];\n\n for (const filePath of filePaths) {\n const sourceFile = program.getSourceFile(filePath);\n if (!sourceFile) continue;\n const fileResults = extractFromSourceFile(sourceFile, checker);\n results.push(...fileResults);\n }\n\n return results;\n}\n\nfunction extractFromSourceFile(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker\n): StepDefinitionMeta[] {\n const results: StepDefinitionMeta[] = [];\n\n function visit(node: ts.Node) {\n // The chain now terminates at `.step(...)`, which is the registration\n // point (there is no `.register()` anymore).\n if (\n ts.isCallExpression(node) &&\n ts.isPropertyAccessExpression(node.expression) &&\n node.expression.name.text === \"step\"\n ) {\n const meta = extractFromRegisterCall(node, sourceFile, checker);\n if (meta) {\n results.push(meta);\n }\n }\n ts.forEachChild(node, visit);\n }\n\n visit(sourceFile);\n return results;\n}\n\nfunction extractFromRegisterCall(\n registerCall: ts.CallExpression,\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker\n): StepDefinitionMeta | null {\n // Walk backwards through the method chain to find all parts\n // Pattern: builder().statement(...).dependencies?(...).step(...).register()\n // Or: Variable(\"...\").dependencies?(...).step(...).register()\n\n const chain = collectCallChain(registerCall);\n\n let stepType: StepType | null = null;\n let expression: string | null = null;\n let dependencies: StepDefinitionMeta[\"dependencies\"] = {\n given: {},\n when: {},\n then: {},\n };\n let produces: string[] = [];\n\n for (const link of chain) {\n const name = getCallName(link);\n if (!name) continue;\n\n if (name === \"register\") {\n // Already at register, continue\n continue;\n }\n\n if (name === \"step\") {\n produces = extractProducedKeys(link, checker);\n continue;\n }\n\n if (name === \"dependencies\") {\n dependencies = extractDependencies(link);\n continue;\n }\n\n if (name === \"statement\") {\n expression = extractExpression(link);\n // Try to find the builder type by continuing up the chain\n continue;\n }\n\n // Check if this is a builder call like givenBuilder()\n if (BUILDER_NAMES[name]) {\n stepType = BUILDER_NAMES[name];\n continue;\n }\n }\n\n // If we didn't find the builder or expression in the chain, try the \"re-exported\" pattern:\n // const Given = givenBuilder<T>().statement;\n // Given(\"foo\").step(...).register()\n if (!stepType || !expression) {\n const reExport = resolveReExportedCall(chain, checker);\n if (reExport) {\n if (!stepType) stepType = reExport.stepType;\n if (!expression) expression = reExport.expression;\n }\n }\n\n if (!stepType || !expression) {\n return null;\n }\n\n const line =\n sourceFile.getLineAndCharacterOfPosition(registerCall.getStart()).line + 1;\n\n return {\n stepType,\n expression,\n dependencies,\n produces,\n sourceFile: sourceFile.fileName,\n line,\n };\n}\n\n/**\n * Collect all call expressions in the method chain, from register() back to the origin.\n */\nfunction collectCallChain(call: ts.CallExpression): ts.CallExpression[] {\n const chain: ts.CallExpression[] = [call];\n let current: ts.Expression = call.expression;\n\n while (true) {\n // Walk through PropertyAccessExpression to find the next call\n if (ts.isPropertyAccessExpression(current)) {\n current = current.expression;\n }\n\n if (ts.isCallExpression(current)) {\n chain.push(current);\n current = current.expression;\n } else {\n break;\n }\n }\n\n return chain;\n}\n\nfunction getCallName(call: ts.CallExpression): string | null {\n const expr = call.expression;\n\n // .method() pattern\n if (ts.isPropertyAccessExpression(expr)) {\n return expr.name.text;\n }\n\n // direct call: functionName()\n if (ts.isIdentifier(expr)) {\n return expr.text;\n }\n\n return null;\n}\n\nfunction extractExpression(statementCall: ts.CallExpression): string | null {\n const arg = statementCall.arguments[0];\n if (!arg) return null;\n\n // String literal: .statement(\"a user\")\n if (ts.isStringLiteral(arg)) {\n return arg.text;\n }\n\n // Arrow function with template literal: .statement((name: string) => `a user named ${name}`)\n if (ts.isArrowFunction(arg)) {\n return extractExpressionFromArrowFunction(arg);\n }\n\n return null;\n}\n\nfunction extractExpressionFromArrowFunction(\n fn: ts.ArrowFunction\n): string | null {\n const params = fn.parameters.map((p) => p.name.getText());\n\n // The body should be a template expression or string literal\n let body = fn.body;\n\n // If wrapped in a block with a return, unwrap\n if (ts.isBlock(body)) {\n const returnStmt = body.statements.find(ts.isReturnStatement);\n if (returnStmt?.expression) {\n body = returnStmt.expression;\n } else {\n return null;\n }\n }\n\n if (ts.isTemplateExpression(body)) {\n return reconstructExpressionFromTemplate(body, params);\n }\n\n if (ts.isNoSubstitutionTemplateLiteral(body)) {\n return body.text;\n }\n\n return null;\n}\n\nfunction reconstructExpressionFromTemplate(\n template: ts.TemplateExpression,\n paramNames: string[]\n): string {\n let result = template.head.text;\n\n for (const span of template.templateSpans) {\n if (ts.isIdentifier(span.expression) && paramNames.includes(span.expression.text)) {\n result += \"{string}\";\n } else {\n // Non-parameter expression, use {string} as fallback\n result += \"{string}\";\n }\n result += span.literal.text;\n }\n\n return result;\n}\n\nfunction extractDependencies(\n depsCall: ts.CallExpression\n): StepDefinitionMeta[\"dependencies\"] {\n const deps: StepDefinitionMeta[\"dependencies\"] = {\n given: {},\n when: {},\n then: {},\n };\n\n const arg = depsCall.arguments[0];\n if (!arg || !ts.isObjectLiteralExpression(arg)) return deps;\n\n for (const prop of arg.properties) {\n if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))\n continue;\n\n const phase = prop.name.text as \"given\" | \"when\" | \"then\";\n if (!deps[phase]) continue;\n\n if (ts.isObjectLiteralExpression(prop.initializer)) {\n for (const innerProp of prop.initializer.properties) {\n if (\n ts.isPropertyAssignment(innerProp) &&\n ts.isIdentifier(innerProp.name) &&\n ts.isStringLiteral(innerProp.initializer)\n ) {\n const val = innerProp.initializer.text;\n if (val === \"required\" || val === \"optional\") {\n deps[phase][innerProp.name.text] = val;\n }\n }\n }\n }\n }\n\n return deps;\n}\n\nfunction extractProducedKeys(\n stepCall: ts.CallExpression,\n checker: ts.TypeChecker\n): string[] {\n const callback = stepCall.arguments[0];\n if (!callback) return [];\n\n // Try to get the return type of the callback by analyzing its body\n if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) {\n return extractProducedKeysFromCallback(callback, checker);\n }\n\n return [];\n}\n\nfunction extractProducedKeysFromCallback(\n callback: ts.ArrowFunction | ts.FunctionExpression,\n checker: ts.TypeChecker\n): string[] {\n const body = callback.body;\n\n // Concise arrow: () => ({ key: value })\n if (!ts.isBlock(body)) {\n return extractKeysFromExpression(body, checker);\n }\n\n // Block body: look at return statements\n const keys = new Set<string>();\n function visitReturn(node: ts.Node) {\n if (ts.isReturnStatement(node) && node.expression) {\n for (const key of extractKeysFromExpression(node.expression, checker)) {\n keys.add(key);\n }\n }\n ts.forEachChild(node, visitReturn);\n }\n visitReturn(body);\n return [...keys];\n}\n\nfunction extractKeysFromExpression(\n expr: ts.Expression,\n checker: ts.TypeChecker\n): string[] {\n // Unwrap parenthesized expressions\n while (ts.isParenthesizedExpression(expr)) {\n expr = expr.expression;\n }\n\n // Object literal: { user: ..., token: ... }\n if (ts.isObjectLiteralExpression(expr)) {\n return expr.properties\n .filter(\n (p): p is ts.PropertyAssignment | ts.ShorthandPropertyAssignment =>\n ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)\n )\n .map((p) => p.name.getText())\n .filter(Boolean);\n }\n\n // Try type checker as fallback\n try {\n const type = checker.getTypeAtLocation(expr);\n return type\n .getProperties()\n .map((p) => p.name)\n .filter((n) => n !== \"merge\");\n } catch {\n return [];\n }\n}\n\ninterface ReExportResult {\n stepType: StepType;\n expression: string | null;\n}\n\nfunction resolveReExportedCall(\n chain: ts.CallExpression[],\n checker: ts.TypeChecker\n): ReExportResult | null {\n // Look for the pattern: Variable(\"...\")... where Variable was assigned from builderType().statement\n // The last call in the chain (furthest from register) should be the variable call\n\n const lastCall = chain[chain.length - 1];\n if (!lastCall) return null;\n\n const expr = lastCall.expression;\n\n // If it's an identifier (like \"Given\", \"When\", \"Then\"), trace its declaration\n let identifier: ts.Identifier | null = null;\n if (ts.isIdentifier(expr)) {\n identifier = expr;\n } else if (\n ts.isPropertyAccessExpression(expr) &&\n ts.isIdentifier(expr.expression)\n ) {\n identifier = expr.expression;\n }\n\n if (!identifier) return null;\n\n const symbol = checker.getSymbolAtLocation(identifier);\n if (!symbol) return null;\n\n const decl = symbol.valueDeclaration;\n if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer)\n return null;\n\n // Check if initializer is builderType<T>().statement\n const init = decl.initializer;\n\n // Pattern: givenBuilder<T>().statement (PropertyAccessExpression)\n if (ts.isPropertyAccessExpression(init) && init.name.text === \"statement\") {\n const callExpr = init.expression;\n if (ts.isCallExpression(callExpr)) {\n const callee = callExpr.expression;\n if (ts.isIdentifier(callee) && BUILDER_NAMES[callee.text]) {\n // The lastCall IS the statement call — extract expression from it\n const expression = extractExpression(lastCall);\n return {\n stepType: BUILDER_NAMES[callee.text],\n expression,\n };\n }\n }\n }\n\n return null;\n}\n","import { MatchedStep, ParsedScenario, StepDefinitionMeta } from \"./types.js\";\n\ninterface CompiledPattern {\n regex: RegExp;\n definition: StepDefinitionMeta;\n}\n\nfunction compileDefinitions(\n definitions: StepDefinitionMeta[]\n): CompiledPattern[] {\n const compiled: CompiledPattern[] = [];\n for (const def of definitions) {\n try {\n // Replace {paramType} placeholders with (.+) to match any value,\n // regardless of which parser placeholder ({string}, {int}, ...) the\n // step definition declared.\n const placeholder = \"###PLACEHOLDER###\";\n const regexStr = def.expression\n .replace(/\\{[^}]+\\}/g, placeholder)\n .replace(/[.*+?^$()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(new RegExp(placeholder, \"g\"), \"(.+)\");\n compiled.push({\n regex: new RegExp(`^${regexStr}$`, \"i\"),\n definition: def,\n });\n } catch {\n // Skip definitions with invalid expressions\n }\n }\n return compiled;\n}\n\nexport function matchScenarioSteps(\n scenario: ParsedScenario,\n definitions: StepDefinitionMeta[]\n): MatchedStep[] {\n const compiled = compileDefinitions(definitions);\n\n return scenario.steps.map(step => {\n const matches = findMatches(step.text, step.effectiveKeyword, compiled);\n return {\n ...step,\n definitions: matches,\n };\n });\n}\n\nexport function findMatchingDefinitions(\n text: string,\n effectiveKeyword: \"Given\" | \"When\" | \"Then\",\n definitions: StepDefinitionMeta[]\n): StepDefinitionMeta[] {\n const compiled = compileDefinitions(definitions);\n return findMatches(text, effectiveKeyword, compiled);\n}\n\nfunction findMatches(\n text: string,\n effectiveKeyword: \"Given\" | \"When\" | \"Then\",\n compiled: CompiledPattern[]\n): StepDefinitionMeta[] {\n const keywordToStepType: Record<string, string> = {\n Given: \"given\",\n When: \"when\",\n Then: \"then\",\n };\n const expectedStepType = keywordToStepType[effectiveKeyword];\n\n // First try to match with the correct step type\n const typedMatches: StepDefinitionMeta[] = [];\n for (const { regex, definition } of compiled) {\n if (definition.stepType !== expectedStepType) continue;\n if (regex.test(text)) typedMatches.push(definition);\n }\n\n if (typedMatches.length > 0) return typedMatches;\n\n // Fallback: match any step type\n const fallbackMatches: StepDefinitionMeta[] = [];\n for (const { regex, definition } of compiled) {\n if (regex.test(text)) fallbackMatches.push(definition);\n }\n\n return fallbackMatches;\n}\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\n\nexport const ambiguousStepRule: AnalysisRule = {\n name: \"ambiguous-step\",\n\n check(\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n ): Diagnostic[] {\n return matchedSteps\n .filter((step) => step.definitions.length > 1)\n .map((step) => {\n const locations = step.definitions\n .map((d) => `${d.sourceFile}:${d.line}`)\n .join(\", \");\n return {\n file: scenario.file,\n range: {\n startLine: step.line,\n startColumn: step.column,\n endLine: step.line,\n endColumn: step.column + step.text.length,\n },\n severity: \"error\" as const,\n message: `Step \"${step.text}\" matches multiple step definitions: ${locations}`,\n rule: \"ambiguous-step\",\n source: \"step-forge\" as const,\n };\n });\n },\n};\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\n\nexport const dependencyRule: AnalysisRule = {\n name: \"dependency-check\",\n\n check(\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n ): Diagnostic[] {\n const diagnostics: Diagnostic[] = [];\n const produced = {\n given: new Set<string>(),\n when: new Set<string>(),\n then: new Set<string>(),\n };\n\n for (const step of matchedSteps) {\n if (step.definitions.length !== 1) continue;\n\n const { dependencies, produces, stepType } = step.definitions[0];\n\n // Collect all missing required dependencies for this step\n const missing: Record<string, string[]> = {};\n for (const phase of [\"given\", \"when\", \"then\"] as const) {\n for (const [key, requirement] of Object.entries(dependencies[phase])) {\n if (requirement === \"required\" && !produced[phase].has(key)) {\n if (!missing[phase]) {\n missing[phase] = [];\n }\n missing[phase].push(key);\n }\n }\n }\n\n if (Object.keys(missing).length > 0) {\n const lines = Object.entries(missing).map(\n ([phase, keys]) =>\n `${phase.charAt(0).toUpperCase() + phase.slice(1)}: ${keys.map((k) => `${phase}.${k}`).join(\", \")}`\n );\n const message =\n \"Missing required dependencies:\\n\" + lines.join(\"\\n\");\n\n diagnostics.push({\n file: scenario.file,\n range: {\n startLine: step.line,\n startColumn: step.column,\n endLine: step.line,\n endColumn: step.column + step.text.length,\n },\n severity: \"error\",\n message,\n rule: \"dependency-check\",\n source: \"step-forge\",\n });\n }\n\n // Add produced keys for this step\n for (const key of produces) {\n produced[stepType].add(key);\n }\n }\n\n return diagnostics;\n },\n};\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\n\nexport const undefinedStepRule: AnalysisRule = {\n name: \"undefined-step\",\n\n check(\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n ): Diagnostic[] {\n return matchedSteps\n .filter((step) => step.definitions.length === 0)\n .map((step) => ({\n file: scenario.file,\n range: {\n startLine: step.line,\n startColumn: step.column,\n endLine: step.line,\n endColumn: step.column + step.text.length,\n },\n severity: \"error\" as const,\n message: `Step \"${step.text}\" does not match any step definition`,\n rule: \"undefined-step\",\n source: \"step-forge\" as const,\n }));\n },\n};\n","import {\n AnalysisRule,\n Diagnostic,\n MatchedStep,\n ParsedScenario,\n} from \"../types.js\";\nimport { ambiguousStepRule } from \"./ambiguousStepRule.js\";\nimport { dependencyRule } from \"./dependencyRule.js\";\nimport { undefinedStepRule } from \"./undefinedStepRule.js\";\n\nexport const defaultRules: AnalysisRule[] = [\n undefinedStepRule,\n ambiguousStepRule,\n dependencyRule,\n];\n\nexport function runRules(\n rules: AnalysisRule[],\n scenario: ParsedScenario,\n matchedSteps: MatchedStep[]\n): Diagnostic[] {\n return rules.flatMap((rule) => rule.check(scenario, matchedSteps));\n}\n","import { glob } from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { extractStepDefinitions } from \"./stepExtractor.js\";\nimport { parseFeatureFiles, parseFeatureContent } from \"./gherkinParser.js\";\nimport {\n matchScenarioSteps,\n findMatchingDefinitions,\n} from \"./stepMatcher.js\";\nimport { defaultRules, runRules } from \"./rules/index.js\";\nimport type {\n AnalyzerConfig,\n AnalysisRule,\n Diagnostic,\n StepDefinitionMeta,\n ParsedScenario,\n MatchedStep,\n ParsedStep,\n} from \"./types.js\";\n\nexport type {\n AnalyzerConfig,\n AnalysisRule,\n Diagnostic,\n StepDefinitionMeta,\n ParsedScenario,\n MatchedStep,\n ParsedStep,\n};\n\nexport {\n extractStepDefinitions,\n parseFeatureFiles,\n parseFeatureContent,\n matchScenarioSteps,\n findMatchingDefinitions,\n defaultRules,\n};\n\nexport interface AnalyzeOptions {\n rules?: AnalysisRule[];\n}\n\nexport async function analyze(\n config: AnalyzerConfig,\n options?: AnalyzeOptions\n): Promise<Diagnostic[]> {\n const rules = options?.rules ?? defaultRules;\n\n // 1. Resolve file globs to paths\n const stepFilePaths = await resolveGlobs(config.stepFiles);\n const featureFilePaths = await resolveGlobs(config.featureFiles);\n\n if (stepFilePaths.length === 0) {\n return [];\n }\n if (featureFilePaths.length === 0) {\n return [];\n }\n\n // 2. Extract step metadata from step files\n const stepDefinitions = extractStepDefinitions(\n stepFilePaths,\n config.tsConfigPath\n );\n\n // 3. Parse feature files\n const scenarios = parseFeatureFiles(featureFilePaths);\n\n // 4. For each scenario, match steps and run rules\n const diagnostics: Diagnostic[] = [];\n for (const scenario of scenarios) {\n const matchedSteps = matchScenarioSteps(scenario, stepDefinitions);\n const scenarioDiags = runRules(rules, scenario, matchedSteps);\n diagnostics.push(...scenarioDiags);\n }\n\n return diagnostics;\n}\n\nasync function resolveGlobs(patterns: string[]): Promise<string[]> {\n const files: string[] = [];\n for (const pattern of patterns) {\n for await (const file of glob(pattern)) {\n files.push(path.resolve(file));\n }\n }\n // Deduplicate\n return [...new Set(files)];\n}\n"],"mappings":";;;;;AAKA,MAAM,gBAA0C;CAC9C,cAAc;CACd,aAAa;CACb,aAAa;AACf;AAEA,SAAgB,uBACd,WACA,cACsB;CACtB,MAAM,aACJ,gBAAgB,GAAG,eAAe,QAAQ,IAAI,GAAG,GAAG,IAAI,UAAU;CACpE,IAAI,kBAAsC;EACxC,QAAQ,GAAG,aAAa;EACxB,QAAQ,GAAG,WAAW;EACtB,kBAAkB,GAAG,qBAAqB;EAC1C,QAAQ;EACR,iBAAiB;CACnB;CAEA,IAAI,YAAY;EACd,MAAM,aAAa,GAAG,eAAe,YAAY,GAAG,IAAI,QAAQ;EAChE,IAAI,CAAC,WAAW,OAMd,kBALe,GAAG,2BAChB,WAAW,QACX,GAAG,KACH,WAAW,QAAQ,iBAAiB,EAAE,CAEjB,CAAC,CAAC;CAE7B;CAGA,gBAAgB,SAAS;CAEzB,MAAM,UAAU,GAAG,cAAc,WAAW,eAAe;CAC3D,MAAM,UAAU,QAAQ,eAAe;CACvC,MAAM,UAAgC,CAAC;CAEvC,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,aAAa,QAAQ,cAAc,QAAQ;EACjD,IAAI,CAAC,YAAY;EACjB,MAAM,cAAc,sBAAsB,YAAY,OAAO;EAC7D,QAAQ,KAAK,GAAG,WAAW;CAC7B;CAEA,OAAO;AACT;AAEA,SAAS,sBACP,YACA,SACsB;CACtB,MAAM,UAAgC,CAAC;CAEvC,SAAS,MAAM,MAAe;EAG5B,IACE,GAAG,iBAAiB,IAAI,KACxB,GAAG,2BAA2B,KAAK,UAAU,KAC7C,KAAK,WAAW,KAAK,SAAS,QAC9B;GACA,MAAM,OAAO,wBAAwB,MAAM,YAAY,OAAO;GAC9D,IAAI,MACF,QAAQ,KAAK,IAAI;EAErB;EACA,GAAG,aAAa,MAAM,KAAK;CAC7B;CAEA,MAAM,UAAU;CAChB,OAAO;AACT;AAEA,SAAS,wBACP,cACA,YACA,SAC2B;CAK3B,MAAM,QAAQ,iBAAiB,YAAY;CAE3C,IAAI,WAA4B;CAChC,IAAI,aAA4B;CAChC,IAAI,eAAmD;EACrD,OAAO,CAAC;EACR,MAAM,CAAC;EACP,MAAM,CAAC;CACT;CACA,IAAI,WAAqB,CAAC;CAE1B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,YAAY,IAAI;EAC7B,IAAI,CAAC,MAAM;EAEX,IAAI,SAAS,YAEX;EAGF,IAAI,SAAS,QAAQ;GACnB,WAAW,oBAAoB,MAAM,OAAO;GAC5C;EACF;EAEA,IAAI,SAAS,gBAAgB;GAC3B,eAAe,oBAAoB,IAAI;GACvC;EACF;EAEA,IAAI,SAAS,aAAa;GACxB,aAAa,kBAAkB,IAAI;GAEnC;EACF;EAGA,IAAI,cAAc,OAAO;GACvB,WAAW,cAAc;GACzB;EACF;CACF;CAKA,IAAI,CAAC,YAAY,CAAC,YAAY;EAC5B,MAAM,WAAW,sBAAsB,OAAO,OAAO;EACrD,IAAI,UAAU;GACZ,IAAI,CAAC,UAAU,WAAW,SAAS;GACnC,IAAI,CAAC,YAAY,aAAa,SAAS;EACzC;CACF;CAEA,IAAI,CAAC,YAAY,CAAC,YAChB,OAAO;CAGT,MAAM,OACJ,WAAW,8BAA8B,aAAa,SAAS,CAAC,CAAC,CAAC,OAAO;CAE3E,OAAO;EACL;EACA;EACA;EACA;EACA,YAAY,WAAW;EACvB;CACF;AACF;;;;AAKA,SAAS,iBAAiB,MAA8C;CACtE,MAAM,QAA6B,CAAC,IAAI;CACxC,IAAI,UAAyB,KAAK;CAElC,OAAO,MAAM;EAEX,IAAI,GAAG,2BAA2B,OAAO,GACvC,UAAU,QAAQ;EAGpB,IAAI,GAAG,iBAAiB,OAAO,GAAG;GAChC,MAAM,KAAK,OAAO;GAClB,UAAU,QAAQ;EACpB,OACE;CAEJ;CAEA,OAAO;AACT;AAEA,SAAS,YAAY,MAAwC;CAC3D,MAAM,OAAO,KAAK;CAGlB,IAAI,GAAG,2BAA2B,IAAI,GACpC,OAAO,KAAK,KAAK;CAInB,IAAI,GAAG,aAAa,IAAI,GACtB,OAAO,KAAK;CAGd,OAAO;AACT;AAEA,SAAS,kBAAkB,eAAiD;CAC1E,MAAM,MAAM,cAAc,UAAU;CACpC,IAAI,CAAC,KAAK,OAAO;CAGjB,IAAI,GAAG,gBAAgB,GAAG,GACxB,OAAO,IAAI;CAIb,IAAI,GAAG,gBAAgB,GAAG,GACxB,OAAO,mCAAmC,GAAG;CAG/C,OAAO;AACT;AAEA,SAAS,mCACP,IACe;CACf,MAAM,SAAS,GAAG,WAAW,KAAK,MAAM,EAAE,KAAK,QAAQ,CAAC;CAGxD,IAAI,OAAO,GAAG;CAGd,IAAI,GAAG,QAAQ,IAAI,GAAG;EACpB,MAAM,aAAa,KAAK,WAAW,KAAK,GAAG,iBAAiB;EAC5D,IAAI,YAAY,YACd,OAAO,WAAW;OAElB,OAAO;CAEX;CAEA,IAAI,GAAG,qBAAqB,IAAI,GAC9B,OAAO,kCAAkC,MAAM,MAAM;CAGvD,IAAI,GAAG,gCAAgC,IAAI,GACzC,OAAO,KAAK;CAGd,OAAO;AACT;AAEA,SAAS,kCACP,UACA,YACQ;CACR,IAAI,SAAS,SAAS,KAAK;CAE3B,KAAK,MAAM,QAAQ,SAAS,eAAe;EACzC,IAAI,GAAG,aAAa,KAAK,UAAU,KAAK,WAAW,SAAS,KAAK,WAAW,IAAI,GAC9E,UAAU;OAGV,UAAU;EAEZ,UAAU,KAAK,QAAQ;CACzB;CAEA,OAAO;AACT;AAEA,SAAS,oBACP,UACoC;CACpC,MAAM,OAA2C;EAC/C,OAAO,CAAC;EACR,MAAM,CAAC;EACP,MAAM,CAAC;CACT;CAEA,MAAM,MAAM,SAAS,UAAU;CAC/B,IAAI,CAAC,OAAO,CAAC,GAAG,0BAA0B,GAAG,GAAG,OAAO;CAEvD,KAAK,MAAM,QAAQ,IAAI,YAAY;EACjC,IAAI,CAAC,GAAG,qBAAqB,IAAI,KAAK,CAAC,GAAG,aAAa,KAAK,IAAI,GAC9D;EAEF,MAAM,QAAQ,KAAK,KAAK;EACxB,IAAI,CAAC,KAAK,QAAQ;EAElB,IAAI,GAAG,0BAA0B,KAAK,WAAW;QAC1C,MAAM,aAAa,KAAK,YAAY,YACvC,IACE,GAAG,qBAAqB,SAAS,KACjC,GAAG,aAAa,UAAU,IAAI,KAC9B,GAAG,gBAAgB,UAAU,WAAW,GACxC;IACA,MAAM,MAAM,UAAU,YAAY;IAClC,IAAI,QAAQ,cAAc,QAAQ,YAChC,KAAK,MAAM,CAAC,UAAU,KAAK,QAAQ;GAEvC;;CAGN;CAEA,OAAO;AACT;AAEA,SAAS,oBACP,UACA,SACU;CACV,MAAM,WAAW,SAAS,UAAU;CACpC,IAAI,CAAC,UAAU,OAAO,CAAC;CAGvB,IAAI,GAAG,gBAAgB,QAAQ,KAAK,GAAG,qBAAqB,QAAQ,GAClE,OAAO,gCAAgC,UAAU,OAAO;CAG1D,OAAO,CAAC;AACV;AAEA,SAAS,gCACP,UACA,SACU;CACV,MAAM,OAAO,SAAS;CAGtB,IAAI,CAAC,GAAG,QAAQ,IAAI,GAClB,OAAO,0BAA0B,MAAM,OAAO;CAIhD,MAAM,uBAAO,IAAI,IAAY;CAC7B,SAAS,YAAY,MAAe;EAClC,IAAI,GAAG,kBAAkB,IAAI,KAAK,KAAK,YACrC,KAAK,MAAM,OAAO,0BAA0B,KAAK,YAAY,OAAO,GAClE,KAAK,IAAI,GAAG;EAGhB,GAAG,aAAa,MAAM,WAAW;CACnC;CACA,YAAY,IAAI;CAChB,OAAO,CAAC,GAAG,IAAI;AACjB;AAEA,SAAS,0BACP,MACA,SACU;CAEV,OAAO,GAAG,0BAA0B,IAAI,GACtC,OAAO,KAAK;CAId,IAAI,GAAG,0BAA0B,IAAI,GACnC,OAAO,KAAK,WACT,QACE,MACC,GAAG,qBAAqB,CAAC,KAAK,GAAG,8BAA8B,CAAC,CACpE,CAAC,CACA,KAAK,MAAM,EAAE,KAAK,QAAQ,CAAC,CAAC,CAC5B,OAAO,OAAO;CAInB,IAAI;EAEF,OADa,QAAQ,kBAAkB,IAC7B,CAAC,CACR,cAAc,CAAC,CACf,KAAK,MAAM,EAAE,IAAI,CAAC,CAClB,QAAQ,MAAM,MAAM,OAAO;CAChC,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAOA,SAAS,sBACP,OACA,SACuB;CAIvB,MAAM,WAAW,MAAM,MAAM,SAAS;CACtC,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,OAAO,SAAS;CAGtB,IAAI,aAAmC;CACvC,IAAI,GAAG,aAAa,IAAI,GACtB,aAAa;MACR,IACL,GAAG,2BAA2B,IAAI,KAClC,GAAG,aAAa,KAAK,UAAU,GAE/B,aAAa,KAAK;CAGpB,IAAI,CAAC,YAAY,OAAO;CAExB,MAAM,SAAS,QAAQ,oBAAoB,UAAU;CACrD,IAAI,CAAC,QAAQ,OAAO;CAEpB,MAAM,OAAO,OAAO;CACpB,IAAI,CAAC,QAAQ,CAAC,GAAG,sBAAsB,IAAI,KAAK,CAAC,KAAK,aACpD,OAAO;CAGT,MAAM,OAAO,KAAK;CAGlB,IAAI,GAAG,2BAA2B,IAAI,KAAK,KAAK,KAAK,SAAS,aAAa;EACzE,MAAM,WAAW,KAAK;EACtB,IAAI,GAAG,iBAAiB,QAAQ,GAAG;GACjC,MAAM,SAAS,SAAS;GACxB,IAAI,GAAG,aAAa,MAAM,KAAK,cAAc,OAAO,OAAO;IAEzD,MAAM,aAAa,kBAAkB,QAAQ;IAC7C,OAAO;KACL,UAAU,cAAc,OAAO;KAC/B;IACF;GACF;EACF;CACF;CAEA,OAAO;AACT;;;ACzaA,SAAS,mBACP,aACmB;CACnB,MAAM,WAA8B,CAAC;CACrC,KAAK,MAAM,OAAO,aAChB,IAAI;EAIF,MAAM,cAAc;EACpB,MAAM,WAAW,IAAI,WAClB,QAAQ,cAAc,WAAW,CAAC,CAClC,QAAQ,qBAAqB,MAAM,CAAC,CACpC,QAAQ,IAAI,OAAO,aAAa,GAAG,GAAG,MAAM;EAC/C,SAAS,KAAK;GACZ,OAAO,IAAI,OAAO,IAAI,SAAS,IAAI,GAAG;GACtC,YAAY;EACd,CAAC;CACH,QAAQ,CAER;CAEF,OAAO;AACT;AAEA,SAAgB,mBACd,UACA,aACe;CACf,MAAM,WAAW,mBAAmB,WAAW;CAE/C,OAAO,SAAS,MAAM,KAAI,SAAQ;EAChC,MAAM,UAAU,YAAY,KAAK,MAAM,KAAK,kBAAkB,QAAQ;EACtE,OAAO;GACL,GAAG;GACH,aAAa;EACf;CACF,CAAC;AACH;AAEA,SAAgB,wBACd,MACA,kBACA,aACsB;CAEtB,OAAO,YAAY,MAAM,kBADR,mBAAmB,WACc,CAAC;AACrD;AAEA,SAAS,YACP,MACA,kBACA,UACsB;CAMtB,MAAM,mBAAmB;EAJvB,OAAO;EACP,MAAM;EACN,MAAM;CAEiC,EAAE;CAG3C,MAAM,eAAqC,CAAC;CAC5C,KAAK,MAAM,EAAE,OAAO,gBAAgB,UAAU;EAC5C,IAAI,WAAW,aAAa,kBAAkB;EAC9C,IAAI,MAAM,KAAK,IAAI,GAAG,aAAa,KAAK,UAAU;CACpD;CAEA,IAAI,aAAa,SAAS,GAAG,OAAO;CAGpC,MAAM,kBAAwC,CAAC;CAC/C,KAAK,MAAM,EAAE,OAAO,gBAAgB,UAClC,IAAI,MAAM,KAAK,IAAI,GAAG,gBAAgB,KAAK,UAAU;CAGvD,OAAO;AACT;;;AI1EA,MAAa,eAA+B;CAC1C;EDHA,MAAM;EAEN,MACE,UACA,cACc;GACd,OAAO,aACJ,QAAQ,SAAS,KAAK,YAAY,WAAW,CAAC,CAAC,CAC/C,KAAK,UAAU;IACd,MAAM,SAAS;IACf,OAAO;KACL,WAAW,KAAK;KAChB,aAAa,KAAK;KAClB,SAAS,KAAK;KACd,WAAW,KAAK,SAAS,KAAK,KAAK;IACrC;IACA,UAAU;IACV,SAAS,SAAS,KAAK,KAAK;IAC5B,MAAM;IACN,QAAQ;GACV,EAAE;EACN;CClBA;CACA;EHJA,MAAM;EAEN,MACE,UACA,cACc;GACd,OAAO,aACJ,QAAQ,SAAS,KAAK,YAAY,SAAS,CAAC,CAAC,CAC7C,KAAK,SAAS;IACb,MAAM,YAAY,KAAK,YACpB,KAAK,MAAM,GAAG,EAAE,WAAW,GAAG,EAAE,MAAM,CAAC,CACvC,KAAK,IAAI;IACZ,OAAO;KACL,MAAM,SAAS;KACf,OAAO;MACL,WAAW,KAAK;MAChB,aAAa,KAAK;MAClB,SAAS,KAAK;MACd,WAAW,KAAK,SAAS,KAAK,KAAK;KACrC;KACA,UAAU;KACV,SAAS,SAAS,KAAK,KAAK,uCAAuC;KACnE,MAAM;KACN,QAAQ;IACV;GACF,CAAC;EACL;CGtBA;CACA;EFLA,MAAM;EAEN,MACE,UACA,cACc;GACd,MAAM,cAA4B,CAAC;GACnC,MAAM,WAAW;IACf,uBAAO,IAAI,IAAY;IACvB,sBAAM,IAAI,IAAY;IACtB,sBAAM,IAAI,IAAY;GACxB;GAEA,KAAK,MAAM,QAAQ,cAAc;IAC/B,IAAI,KAAK,YAAY,WAAW,GAAG;IAEnC,MAAM,EAAE,cAAc,UAAU,aAAa,KAAK,YAAY;IAG9D,MAAM,UAAoC,CAAC;IAC3C,KAAK,MAAM,SAAS;KAAC;KAAS;KAAQ;IAAM,GAC1C,KAAK,MAAM,CAAC,KAAK,gBAAgB,OAAO,QAAQ,aAAa,MAAM,GACjE,IAAI,gBAAgB,cAAc,CAAC,SAAS,MAAM,CAAC,IAAI,GAAG,GAAG;KAC3D,IAAI,CAAC,QAAQ,QACX,QAAQ,SAAS,CAAC;KAEpB,QAAQ,MAAM,CAAC,KAAK,GAAG;IACzB;IAIJ,IAAI,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,GAAG;KAKnC,MAAM,UACJ,qCALY,OAAO,QAAQ,OAAO,CAAC,CAAC,KACnC,CAAC,OAAO,UACP,GAAG,MAAM,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,MAAM,MAAM,CAAC,EAAE,IAAI,KAAK,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,IAAI,GAGzD,CAAC,CAAC,KAAK,IAAI;KAEtD,YAAY,KAAK;MACf,MAAM,SAAS;MACf,OAAO;OACL,WAAW,KAAK;OAChB,aAAa,KAAK;OAClB,SAAS,KAAK;OACd,WAAW,KAAK,SAAS,KAAK,KAAK;MACrC;MACA,UAAU;MACV;MACA,MAAM;MACN,QAAQ;KACV,CAAC;IACH;IAGA,KAAK,MAAM,OAAO,UAChB,SAAS,SAAS,CAAC,IAAI,GAAG;GAE9B;GAEA,OAAO;EACT;CExDA;AACF;AAEA,SAAgB,SACd,OACA,UACA,cACc;CACd,OAAO,MAAM,SAAS,SAAS,KAAK,MAAM,UAAU,YAAY,CAAC;AACnE;;;ACoBA,eAAsB,QACpB,QACA,SACuB;CACvB,MAAM,QAAQ,SAAS,SAAS;CAGhC,MAAM,gBAAgB,MAAM,aAAa,OAAO,SAAS;CACzD,MAAM,mBAAmB,MAAM,aAAa,OAAO,YAAY;CAE/D,IAAI,cAAc,WAAW,GAC3B,OAAO,CAAC;CAEV,IAAI,iBAAiB,WAAW,GAC9B,OAAO,CAAC;CAIV,MAAM,kBAAkB,uBACtB,eACA,OAAO,YACT;CAGA,MAAM,YAAY,kBAAkB,gBAAgB;CAGpD,MAAM,cAA4B,CAAC;CACnC,KAAK,MAAM,YAAY,WAAW;EAEhC,MAAM,gBAAgB,SAAS,OAAO,UADjB,mBAAmB,UAAU,eACS,CAAC;EAC5D,YAAY,KAAK,GAAG,aAAa;CACnC;CAEA,OAAO;AACT;AAEA,eAAe,aAAa,UAAuC;CACjE,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,WAAW,UACpB,WAAW,MAAM,QAAQ,KAAK,OAAO,GACnC,MAAM,KAAK,KAAK,QAAQ,IAAI,CAAC;CAIjC,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC;AAC3B"}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as analyze } from "./analyzer-QH03uejK.js";
2
+ import { t as analyze } from "./analyzer-DJyJbU_V.js";
3
3
  //#region src/analyzer/cli.ts
4
4
  function parseArgs(args) {
5
5
  const config = {
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer-cli.js","names":[],"sources":["../../src/analyzer/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { analyze } from \"./index.js\";\nimport type { AnalyzerConfig, Diagnostic } from \"./types.js\";\n\nfunction parseArgs(args: string[]): AnalyzerConfig {\n const config: AnalyzerConfig = {\n stepFiles: [],\n featureFiles: [],\n };\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n const next = args[i + 1];\n\n if ((arg === \"--steps\" || arg === \"-s\") && next) {\n config.stepFiles.push(next);\n i++;\n } else if ((arg === \"--features\" || arg === \"-f\") && next) {\n config.featureFiles.push(next);\n i++;\n } else if (arg === \"--tsconfig\" && next) {\n config.tsConfigPath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n printUsage();\n process.exit(0);\n }\n }\n\n return config;\n}\n\nfunction printUsage() {\n console.log(`Usage: step-forge-analyze [options]\n\nOptions:\n -s, --steps <glob> Glob pattern for step definition files (repeatable)\n -f, --features <glob> Glob pattern for feature files (repeatable)\n --tsconfig <path> Path to tsconfig.json (default: auto-detect)\n -h, --help Show this help message\n\nExample:\n step-forge-analyze --steps \"features/steps/**/*.ts\" --features \"features/**/*.feature\"\n`);\n}\n\nfunction formatDiagnostic(diag: Diagnostic): string {\n const location = `${diag.file}:${diag.range.startLine}:${diag.range.startColumn}`;\n return `${location} - ${diag.severity}: ${diag.message}`;\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const config = parseArgs(args);\n\n if (config.stepFiles.length === 0 || config.featureFiles.length === 0) {\n console.error(\n \"Error: Both --steps and --features are required.\\n\"\n );\n printUsage();\n process.exit(1);\n }\n\n const diagnostics = await analyze(config);\n\n if (diagnostics.length === 0) {\n console.log(\"No issues found.\");\n process.exit(0);\n }\n\n const errors = diagnostics.filter((d) => d.severity === \"error\");\n const warnings = diagnostics.filter((d) => d.severity === \"warning\");\n\n for (const diag of diagnostics) {\n console.log(formatDiagnostic(diag));\n }\n\n console.log(\n `\\nFound ${errors.length} error(s) and ${warnings.length} warning(s).`\n );\n process.exit(errors.length > 0 ? 1 : 0);\n}\n\n// Only run when invoked directly (not when imported by Cucumber or other tools)\nconst isDirectRun =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith(\"analyzer-cli.js\") ||\n process.argv[1]?.endsWith(\"analyzer-cli.ts\");\n\nif (isDirectRun) {\n main().catch((err) => {\n console.error(\"Analyzer failed:\", err);\n process.exit(1);\n });\n}\n"],"mappings":";;;AAKA,SAAS,UAAU,MAAgC;CACjD,MAAM,SAAyB;EAC7B,WAAW,CAAC;EACZ,cAAc,CAAC;CACjB;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,KAAK,IAAI;EAEtB,KAAK,QAAQ,aAAa,QAAQ,SAAS,MAAM;GAC/C,OAAO,UAAU,KAAK,IAAI;GAC1B;EACF,OAAO,KAAK,QAAQ,gBAAgB,QAAQ,SAAS,MAAM;GACzD,OAAO,aAAa,KAAK,IAAI;GAC7B;EACF,OAAO,IAAI,QAAQ,gBAAgB,MAAM;GACvC,OAAO,eAAe;GACtB;EACF,OAAO,IAAI,QAAQ,YAAY,QAAQ,MAAM;GAC3C,WAAW;GACX,QAAQ,KAAK,CAAC;EAChB;CACF;CAEA,OAAO;AACT;AAEA,SAAS,aAAa;CACpB,QAAQ,IAAI;;;;;;;;;;CAUb;AACD;AAEA,SAAS,iBAAiB,MAA0B;CAElD,OAAO,GAAG,GADU,KAAK,KAAK,GAAG,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM,cACjD,KAAK,KAAK,SAAS,IAAI,KAAK;AACjD;AAEA,eAAe,OAAO;CAEpB,MAAM,SAAS,UADF,QAAQ,KAAK,MAAM,CACJ,CAAC;CAE7B,IAAI,OAAO,UAAU,WAAW,KAAK,OAAO,aAAa,WAAW,GAAG;EACrE,QAAQ,MACN,oDACF;EACA,WAAW;EACX,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,cAAc,MAAM,QAAQ,MAAM;CAExC,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IAAI,kBAAkB;EAC9B,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,SAAS,YAAY,QAAQ,MAAM,EAAE,aAAa,OAAO;CAC/D,MAAM,WAAW,YAAY,QAAQ,MAAM,EAAE,aAAa,SAAS;CAEnE,KAAK,MAAM,QAAQ,aACjB,QAAQ,IAAI,iBAAiB,IAAI,CAAC;CAGpC,QAAQ,IACN,WAAW,OAAO,OAAO,gBAAgB,SAAS,OAAO,aAC3D;CACA,QAAQ,KAAK,OAAO,SAAS,IAAI,IAAI,CAAC;AACxC;AAQA,IAJE,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,QAC3C,QAAQ,KAAK,IAAI,SAAS,iBAAiB,KAC3C,QAAQ,KAAK,IAAI,SAAS,iBAAiB,GAG3C,KAAK,EAAE,OAAO,QAAQ;CACpB,QAAQ,MAAM,oBAAoB,GAAG;CACrC,QAAQ,KAAK,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"analyzer-cli.js","names":[],"sources":["../../src/analyzer/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { analyze } from \"./index.js\";\nimport type { AnalyzerConfig, Diagnostic } from \"./types.js\";\n\nfunction parseArgs(args: string[]): AnalyzerConfig {\n const config: AnalyzerConfig = {\n stepFiles: [],\n featureFiles: [],\n };\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n const next = args[i + 1];\n\n if ((arg === \"--steps\" || arg === \"-s\") && next) {\n config.stepFiles.push(next);\n i++;\n } else if ((arg === \"--features\" || arg === \"-f\") && next) {\n config.featureFiles.push(next);\n i++;\n } else if (arg === \"--tsconfig\" && next) {\n config.tsConfigPath = next;\n i++;\n } else if (arg === \"--help\" || arg === \"-h\") {\n printUsage();\n process.exit(0);\n }\n }\n\n return config;\n}\n\nfunction printUsage() {\n console.log(`Usage: step-forge-analyze [options]\n\nOptions:\n -s, --steps <glob> Glob pattern for step definition files (repeatable)\n -f, --features <glob> Glob pattern for feature files (repeatable)\n --tsconfig <path> Path to tsconfig.json (default: auto-detect)\n -h, --help Show this help message\n\nExample:\n step-forge-analyze --steps \"features/steps/**/*.ts\" --features \"features/**/*.feature\"\n`);\n}\n\nfunction formatDiagnostic(diag: Diagnostic): string {\n const location = `${diag.file}:${diag.range.startLine}:${diag.range.startColumn}`;\n return `${location} - ${diag.severity}: ${diag.message}`;\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n const config = parseArgs(args);\n\n if (config.stepFiles.length === 0 || config.featureFiles.length === 0) {\n console.error(\n \"Error: Both --steps and --features are required.\\n\"\n );\n printUsage();\n process.exit(1);\n }\n\n const diagnostics = await analyze(config);\n\n if (diagnostics.length === 0) {\n console.log(\"No issues found.\");\n process.exit(0);\n }\n\n const errors = diagnostics.filter((d) => d.severity === \"error\");\n const warnings = diagnostics.filter((d) => d.severity === \"warning\");\n\n for (const diag of diagnostics) {\n console.log(formatDiagnostic(diag));\n }\n\n console.log(\n `\\nFound ${errors.length} error(s) and ${warnings.length} warning(s).`\n );\n process.exit(errors.length > 0 ? 1 : 0);\n}\n\n// Only run when invoked directly (not when imported by Cucumber or other tools)\nconst isDirectRun =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith(\"analyzer-cli.js\") ||\n process.argv[1]?.endsWith(\"analyzer-cli.ts\");\n\nif (isDirectRun) {\n main().catch((err) => {\n console.error(\"Analyzer failed:\", err);\n process.exit(1);\n });\n}\n"],"mappings":";;;AAKA,SAAS,UAAU,MAAgC;CACjD,MAAM,SAAyB;EAC7B,WAAW,CAAC;EACZ,cAAc,CAAC;CACjB;CAEA,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,MAAM,OAAO,KAAK,IAAI;EAEtB,KAAK,QAAQ,aAAa,QAAQ,SAAS,MAAM;GAC/C,OAAO,UAAU,KAAK,IAAI;GAC1B;EACF,OAAO,KAAK,QAAQ,gBAAgB,QAAQ,SAAS,MAAM;GACzD,OAAO,aAAa,KAAK,IAAI;GAC7B;EACF,OAAO,IAAI,QAAQ,gBAAgB,MAAM;GACvC,OAAO,eAAe;GACtB;EACF,OAAO,IAAI,QAAQ,YAAY,QAAQ,MAAM;GAC3C,WAAW;GACX,QAAQ,KAAK,CAAC;EAChB;CACF;CAEA,OAAO;AACT;AAEA,SAAS,aAAa;CACpB,QAAQ,IAAI;;;;;;;;;;CAUb;AACD;AAEA,SAAS,iBAAiB,MAA0B;CAElD,OAAO,GAAG,GADU,KAAK,KAAK,GAAG,KAAK,MAAM,UAAU,GAAG,KAAK,MAAM,cACjD,KAAK,KAAK,SAAS,IAAI,KAAK;AACjD;AAEA,eAAe,OAAO;CAEpB,MAAM,SAAS,UADF,QAAQ,KAAK,MAAM,CACJ,CAAC;CAE7B,IAAI,OAAO,UAAU,WAAW,KAAK,OAAO,aAAa,WAAW,GAAG;EACrE,QAAQ,MACN,oDACF;EACA,WAAW;EACX,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,cAAc,MAAM,QAAQ,MAAM;CAExC,IAAI,YAAY,WAAW,GAAG;EAC5B,QAAQ,IAAI,kBAAkB;EAC9B,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,SAAS,YAAY,QAAQ,MAAM,EAAE,aAAa,OAAO;CAC/D,MAAM,WAAW,YAAY,QAAQ,MAAM,EAAE,aAAa,SAAS;CAEnE,KAAK,MAAM,QAAQ,aACjB,QAAQ,IAAI,iBAAiB,IAAI,CAAC;CAGpC,QAAQ,IACN,WAAW,OAAO,OAAO,gBAAgB,SAAS,OAAO,aAC3D;CACA,QAAQ,KAAK,OAAO,SAAS,IAAI,IAAI,CAAC;AACxC;AAQA,IAJE,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,QAC3C,QAAQ,KAAK,EAAE,EAAE,SAAS,iBAAiB,KAC3C,QAAQ,KAAK,EAAE,EAAE,SAAS,iBAAiB,GAG3C,KAAK,CAAC,CAAC,OAAO,QAAQ;CACpB,QAAQ,MAAM,oBAAoB,GAAG;CACrC,QAAQ,KAAK,CAAC;AAChB,CAAC"}
@@ -15,6 +15,20 @@ interface ParsedScenario {
15
15
  name: string;
16
16
  file: string;
17
17
  steps: ParsedStep[];
18
+ /**
19
+ * Gherkin tags in effect for this scenario, each including the leading `@`
20
+ * (feature + scenario tags, plus the Examples-block tags for outline rows).
21
+ * The Vitest plugin maps `@skip`/`@only` onto `test.skip`/`test.only`.
22
+ */
23
+ tags: string[];
24
+ /**
25
+ * Present only for rows expanded from a `Scenario Outline`. Carries the base
26
+ * outline name so the plugin can group its rows under one `describe`, with
27
+ * each row a separate `test` labelled by its example values.
28
+ */
29
+ outline?: {
30
+ name: string;
31
+ };
18
32
  }
19
33
  interface ParsedStep {
20
34
  keyword: "Given" | "When" | "Then" | "And" | "But";
package/dist/analyzer.js CHANGED
@@ -1,2 +1,3 @@
1
- import { a as parseFeatureContent, i as matchScenarioSteps, n as defaultRules, o as parseFeatureFiles, r as findMatchingDefinitions, s as extractStepDefinitions, t as analyze } from "./analyzer-QH03uejK.js";
1
+ import { a as extractStepDefinitions, i as matchScenarioSteps, n as defaultRules, r as findMatchingDefinitions, t as analyze } from "./analyzer-DJyJbU_V.js";
2
+ import { n as parseFeatureFiles, t as parseFeatureContent } from "./gherkinParser-Dp2d7JNr.js";
2
3
  export { analyze, defaultRules, extractStepDefinitions, findMatchingDefinitions, matchScenarioSteps, parseFeatureContent, parseFeatureFiles };
@@ -0,0 +1,116 @@
1
+ import * as fs from "node:fs";
2
+ import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
3
+ import * as messages from "@cucumber/messages";
4
+ //#region src/analyzer/gherkinParser.ts
5
+ function parseFeatureFiles(filePaths) {
6
+ const scenarios = [];
7
+ for (const filePath of filePaths) {
8
+ const parsed = parseFeatureContent(fs.readFileSync(filePath, "utf-8"), filePath);
9
+ scenarios.push(...parsed);
10
+ }
11
+ return scenarios;
12
+ }
13
+ function parseFeatureContent(content, filePath) {
14
+ const feature = new Parser(new AstBuilder(messages.IdGenerator.uuid()), new GherkinClassicTokenMatcher()).parse(content).feature;
15
+ if (!feature) return [];
16
+ const featureBackground = [];
17
+ const scenarios = [];
18
+ const featureTags = tagNames(feature.tags);
19
+ for (const child of feature.children) {
20
+ if (child.background) featureBackground.push(...child.background.steps);
21
+ if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
22
+ if (child.rule) {
23
+ const ruleBackground = [...featureBackground];
24
+ const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
25
+ for (const ruleChild of child.rule.children) {
26
+ if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
27
+ if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
28
+ }
29
+ }
30
+ }
31
+ return scenarios;
32
+ }
33
+ /** Extract tag names (each keeping its leading `@`), deduped in order. */
34
+ function tagNames(tags) {
35
+ return [...new Set((tags ?? []).map((t) => t.name))];
36
+ }
37
+ function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
38
+ const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
39
+ if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
40
+ const bgParsed = convertSteps(backgroundSteps);
41
+ const scenarioParsed = convertSteps(scenario.steps);
42
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
43
+ return [{
44
+ name: scenario.name,
45
+ file: filePath,
46
+ steps: allSteps,
47
+ tags: scenarioTags
48
+ }];
49
+ }
50
+ const results = [];
51
+ for (const example of scenario.examples) {
52
+ if (!example.tableHeader || example.tableBody.length === 0) continue;
53
+ const headers = example.tableHeader.cells.map((c) => c.value);
54
+ const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
55
+ for (const row of example.tableBody) {
56
+ const values = row.cells.map((c) => c.value);
57
+ const substitution = {};
58
+ headers.forEach((h, i) => {
59
+ substitution[h] = values[i];
60
+ });
61
+ const bgParsed = convertSteps(backgroundSteps);
62
+ const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
63
+ ...step,
64
+ text: substituteExampleValues(step.text, substitution)
65
+ }));
66
+ const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
67
+ results.push({
68
+ name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
69
+ file: filePath,
70
+ steps: allSteps,
71
+ tags: exampleTags,
72
+ outline: { name: scenario.name }
73
+ });
74
+ }
75
+ }
76
+ return results;
77
+ }
78
+ function convertSteps(steps) {
79
+ return steps.map((step) => ({
80
+ keyword: normalizeKeyword(step.keyword),
81
+ text: step.text,
82
+ line: step.location.line,
83
+ column: (step.location.column ?? 1) + step.keyword.length
84
+ }));
85
+ }
86
+ function normalizeKeyword(keyword) {
87
+ const trimmed = keyword.trim();
88
+ if (trimmed === "Given") return "Given";
89
+ if (trimmed === "When") return "When";
90
+ if (trimmed === "Then") return "Then";
91
+ if (trimmed === "And") return "And";
92
+ if (trimmed === "But") return "But";
93
+ return "Given";
94
+ }
95
+ function resolveEffectiveKeywords(steps) {
96
+ let lastEffective = "Given";
97
+ return steps.map((step) => {
98
+ let effectiveKeyword;
99
+ if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
100
+ else effectiveKeyword = step.keyword;
101
+ lastEffective = effectiveKeyword;
102
+ return {
103
+ ...step,
104
+ effectiveKeyword
105
+ };
106
+ });
107
+ }
108
+ function substituteExampleValues(text, substitution) {
109
+ let result = text;
110
+ for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
111
+ return result;
112
+ }
113
+ //#endregion
114
+ export { parseFeatureFiles as n, parseFeatureContent as t };
115
+
116
+ //# sourceMappingURL=gherkinParser-Dp2d7JNr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gherkinParser-Dp2d7JNr.js","names":[],"sources":["../../src/analyzer/gherkinParser.ts"],"sourcesContent":["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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"}
@@ -0,0 +1,117 @@
1
+ //#region src/runtime/registry.ts
2
+ /**
3
+ * A collection of registered steps. Deliberately a plain instance (not a hidden
4
+ * module global) so tests and the Vitest plugin can create isolated registries.
5
+ * `globalRegistry` is the default sink that `.step()` writes to.
6
+ */
7
+ var StepRegistry = class {
8
+ steps = [];
9
+ add(step) {
10
+ this.steps.push(step);
11
+ }
12
+ all() {
13
+ return this.steps;
14
+ }
15
+ clear() {
16
+ this.steps = [];
17
+ }
18
+ };
19
+ const globalRegistry = new StepRegistry();
20
+ //#endregion
21
+ //#region src/runtime/hooks.ts
22
+ /**
23
+ * Collection of registered hooks. Like {@link StepRegistry}, a plain instance
24
+ * (not a hidden global) so tests can isolate; `globalHookRegistry` is the
25
+ * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.
26
+ */
27
+ var HookRegistry = class {
28
+ hooks = [];
29
+ add(hook) {
30
+ this.hooks.push(hook);
31
+ }
32
+ /**
33
+ * Hooks for a scope+timing. `before` hooks run in registration order;
34
+ * `after` hooks run in reverse (LIFO), so teardown unwinds setup.
35
+ */
36
+ for(scope, timing) {
37
+ const matching = this.hooks.filter((h) => h.scope === scope && h.timing === timing);
38
+ return timing === "after" ? matching.reverse() : matching;
39
+ }
40
+ clear() {
41
+ this.hooks = [];
42
+ }
43
+ };
44
+ const globalHookRegistry = new HookRegistry();
45
+ /**
46
+ * Run every registered feature/global hook of a scope+timing in order. Used by
47
+ * the generated test modules (feature hooks). Throws if a hook throws, so the
48
+ * runner reports it against the enclosing boundary.
49
+ */
50
+ async function runHooks(scope, timing, registry = globalHookRegistry) {
51
+ for (const hook of registry.for(scope, timing)) await hook.fn();
52
+ }
53
+ const GLOBAL_GUARD = Symbol.for("step-forge.globalHooksStarted");
54
+ /**
55
+ * Run global before-hooks once per worker, ahead of that worker's first
56
+ * scenario, and schedule global after-hooks for worker exit. Idempotent: every
57
+ * feature module calls this in a `beforeAll`, but only the first call in a given
58
+ * worker does anything.
59
+ *
60
+ * Semantics & caveats (the once-per-worker model):
61
+ * - Runs in the *same* realm as steps, so global setup may touch in-process
62
+ * state that steps later read.
63
+ * - "Once per worker", not strictly once per run — with multiple workers it runs
64
+ * in each. Size global setup to be worker-safe (e.g. a server per worker).
65
+ * - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and
66
+ * are not awaited by the runner, so keep them fast/synchronous.
67
+ */
68
+ async function ensureGlobalHooks(registry = globalHookRegistry) {
69
+ const store = globalThis;
70
+ if (store[GLOBAL_GUARD]) return;
71
+ store[GLOBAL_GUARD] = true;
72
+ for (const hook of registry.for("global", "before")) await hook.fn();
73
+ process.once("beforeExit", () => {
74
+ (async () => {
75
+ for (const hook of registry.for("global", "after")) await hook.fn();
76
+ })();
77
+ });
78
+ }
79
+ //#endregion
80
+ Object.defineProperty(exports, "HookRegistry", {
81
+ enumerable: true,
82
+ get: function() {
83
+ return HookRegistry;
84
+ }
85
+ });
86
+ Object.defineProperty(exports, "StepRegistry", {
87
+ enumerable: true,
88
+ get: function() {
89
+ return StepRegistry;
90
+ }
91
+ });
92
+ Object.defineProperty(exports, "ensureGlobalHooks", {
93
+ enumerable: true,
94
+ get: function() {
95
+ return ensureGlobalHooks;
96
+ }
97
+ });
98
+ Object.defineProperty(exports, "globalHookRegistry", {
99
+ enumerable: true,
100
+ get: function() {
101
+ return globalHookRegistry;
102
+ }
103
+ });
104
+ Object.defineProperty(exports, "globalRegistry", {
105
+ enumerable: true,
106
+ get: function() {
107
+ return globalRegistry;
108
+ }
109
+ });
110
+ Object.defineProperty(exports, "runHooks", {
111
+ enumerable: true,
112
+ get: function() {
113
+ return runHooks;
114
+ }
115
+ });
116
+
117
+ //# sourceMappingURL=hooks-CGYzwDOv.cjs.map
@@ -0,0 +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"}
@@ -0,0 +1,82 @@
1
+ //#region src/runtime/registry.ts
2
+ /**
3
+ * A collection of registered steps. Deliberately a plain instance (not a hidden
4
+ * module global) so tests and the Vitest plugin can create isolated registries.
5
+ * `globalRegistry` is the default sink that `.step()` writes to.
6
+ */
7
+ var StepRegistry = class {
8
+ steps = [];
9
+ add(step) {
10
+ this.steps.push(step);
11
+ }
12
+ all() {
13
+ return this.steps;
14
+ }
15
+ clear() {
16
+ this.steps = [];
17
+ }
18
+ };
19
+ const globalRegistry = new StepRegistry();
20
+ //#endregion
21
+ //#region src/runtime/hooks.ts
22
+ /**
23
+ * Collection of registered hooks. Like {@link StepRegistry}, a plain instance
24
+ * (not a hidden global) so tests can isolate; `globalHookRegistry` is the
25
+ * default sink the public `beforeScenario`/`afterAll`/etc helpers write to.
26
+ */
27
+ var HookRegistry = class {
28
+ hooks = [];
29
+ add(hook) {
30
+ this.hooks.push(hook);
31
+ }
32
+ /**
33
+ * Hooks for a scope+timing. `before` hooks run in registration order;
34
+ * `after` hooks run in reverse (LIFO), so teardown unwinds setup.
35
+ */
36
+ for(scope, timing) {
37
+ const matching = this.hooks.filter((h) => h.scope === scope && h.timing === timing);
38
+ return timing === "after" ? matching.reverse() : matching;
39
+ }
40
+ clear() {
41
+ this.hooks = [];
42
+ }
43
+ };
44
+ const globalHookRegistry = new HookRegistry();
45
+ /**
46
+ * Run every registered feature/global hook of a scope+timing in order. Used by
47
+ * the generated test modules (feature hooks). Throws if a hook throws, so the
48
+ * runner reports it against the enclosing boundary.
49
+ */
50
+ async function runHooks(scope, timing, registry = globalHookRegistry) {
51
+ for (const hook of registry.for(scope, timing)) await hook.fn();
52
+ }
53
+ const GLOBAL_GUARD = Symbol.for("step-forge.globalHooksStarted");
54
+ /**
55
+ * Run global before-hooks once per worker, ahead of that worker's first
56
+ * scenario, and schedule global after-hooks for worker exit. Idempotent: every
57
+ * feature module calls this in a `beforeAll`, but only the first call in a given
58
+ * worker does anything.
59
+ *
60
+ * Semantics & caveats (the once-per-worker model):
61
+ * - Runs in the *same* realm as steps, so global setup may touch in-process
62
+ * state that steps later read.
63
+ * - "Once per worker", not strictly once per run — with multiple workers it runs
64
+ * in each. Size global setup to be worker-safe (e.g. a server per worker).
65
+ * - Teardown is best-effort: after-hooks start on the worker's `beforeExit` and
66
+ * are not awaited by the runner, so keep them fast/synchronous.
67
+ */
68
+ async function ensureGlobalHooks(registry = globalHookRegistry) {
69
+ const store = globalThis;
70
+ if (store[GLOBAL_GUARD]) return;
71
+ store[GLOBAL_GUARD] = true;
72
+ for (const hook of registry.for("global", "before")) await hook.fn();
73
+ process.once("beforeExit", () => {
74
+ (async () => {
75
+ for (const hook of registry.for("global", "after")) await hook.fn();
76
+ })();
77
+ });
78
+ }
79
+ //#endregion
80
+ export { StepRegistry as a, runHooks as i, ensureGlobalHooks as n, globalRegistry as o, globalHookRegistry as r, HookRegistry as t };
81
+
82
+ //# sourceMappingURL=hooks-CywugMQQ.js.map