@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.
- package/README.md +2 -0
- package/RUNTIME.md +242 -0
- package/dist/{analyzer-DJyJbU_V.js → analyzer-byS8yRrY.js} +202 -34
- package/dist/analyzer-byS8yRrY.js.map +1 -0
- package/dist/analyzer-cli.js +1 -1
- package/dist/analyzer.d.ts +2 -0
- package/dist/analyzer.js +1 -2
- package/dist/cli.cjs +525 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +526 -0
- package/dist/cli.js.map +1 -0
- package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
- package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
- package/dist/engine-DPVLEHBi.js +163 -0
- package/dist/engine-DPVLEHBi.js.map +1 -0
- package/dist/engine-vqA-eL_T.cjs +186 -0
- package/dist/engine-vqA-eL_T.cjs.map +1 -0
- package/dist/gherkinParser-BT40q_i3.cjs +338 -0
- package/dist/gherkinParser-BT40q_i3.cjs.map +1 -0
- package/dist/gherkinParser-NcttZgN4.js +259 -0
- package/dist/gherkinParser-NcttZgN4.js.map +1 -0
- package/dist/hooks-BDCMKeNq.js +71 -0
- package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
- package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
- package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
- package/dist/runtime.cjs +7 -162
- package/dist/runtime.d.cts +44 -8
- package/dist/runtime.d.ts +44 -8
- package/dist/runtime.js +3 -159
- package/dist/step-forge.cjs +73 -216
- package/dist/step-forge.cjs.map +1 -1
- package/dist/step-forge.d.cts +19 -10
- package/dist/step-forge.d.ts +19 -10
- package/dist/step-forge.js +67 -185
- package/dist/step-forge.js.map +1 -1
- package/package.json +12 -18
- package/dist/analyzer-DJyJbU_V.js.map +0 -1
- package/dist/gherkinParser-Dp2d7JNr.js +0 -116
- package/dist/gherkinParser-Dp2d7JNr.js.map +0 -1
- package/dist/hooks-CywugMQQ.js +0 -82
- package/dist/runtime.cjs.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/vitest.d.ts +0 -74
- package/dist/vitest.js +0 -136
- package/dist/vitest.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyzer-byS8yRrY.js","names":[],"sources":["../../src/globFiles.ts","../../src/analyzer/stepExtractor.ts","../../src/analyzer/gherkinParser.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 { glob } from \"node:fs/promises\";\nimport { stat } from \"node:fs/promises\";\nimport * as path from \"node:path\";\n\n/** Glob metacharacters. A pattern with none of these is a literal path. */\nconst MAGIC = /[*?[\\]{}!()]/;\n\nasync function isFile(p: string): Promise<boolean> {\n try {\n return (await stat(p)).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve glob patterns to a de-duplicated list of absolute file paths, with one\n * important portability guarantee: a **literal absolute path** (no glob magic)\n * is returned directly if it exists, without being handed to `glob()`.\n *\n * This exists because `node:fs`'s `glob` diverges between runtimes — Node\n * matches an absolute-path pattern, Bun returns nothing for one. Rather than\n * depend on that behaviour, we only ever glob relative patterns (against `cwd`)\n * and short-circuit concrete absolute paths ourselves, so callers get identical\n * results under Node and Bun.\n */\nexport async function globFiles(\n patterns: string[],\n cwd: string = process.cwd()\n): Promise<string[]> {\n const files = new Set<string>();\n for (const pattern of patterns) {\n if (path.isAbsolute(pattern) && !MAGIC.test(pattern)) {\n if (await isFile(pattern)) files.add(pattern);\n continue;\n }\n for await (const match of glob(pattern, { cwd })) {\n files.add(path.resolve(cwd, match));\n }\n }\n return [...files];\n}\n","import 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\n/**\n * Threaded through the AST walk. `checker` is `null` on the parse-only fast\n * path; a step that genuinely needs type information sets `needsChecker`, which\n * triggers a one-time retry with a real type-checked `Program`.\n */\ninterface ExtractCtx {\n checker: ts.TypeChecker | null;\n needsChecker: boolean;\n}\n\nexport function extractStepDefinitions(\n filePaths: string[],\n tsConfigPath?: string\n): StepDefinitionMeta[] {\n // Fast path: parse each file with `createSourceFile` (tokenize + parse only,\n // no type resolution — ~1ms/file) and walk the AST. Building a full\n // type-checked `Program` loads `lib.*.d.ts` and resolves every import (~150ms)\n // and is only needed for two uncommon shapes: a `.step()` whose return isn't a\n // plain object literal, or the re-exported-builder pattern. Those set\n // `needsChecker`, and we retry once with a real Program below.\n const sources = filePaths\n .map(parseSourceFile)\n .filter((s): s is ts.SourceFile => s !== null);\n const fast = extractWithSources(sources, null);\n if (!fast.needsChecker) return fast.results;\n\n // Slow path: some step needs type information. Build the program once and\n // re-extract from *its* source files — the checker only understands nodes it\n // bound itself, so the parse-only ASTs above can't be reused here.\n const program = ts.createProgram(\n filePaths,\n resolveCompilerOptions(tsConfigPath)\n );\n const checker = program.getTypeChecker();\n const checkedSources = filePaths\n .map(fp => program.getSourceFile(fp))\n .filter((s): s is ts.SourceFile => s !== undefined);\n return extractWithSources(checkedSources, checker).results;\n}\n\n/** Parse a single file into an AST with no type resolution. */\nfunction parseSourceFile(filePath: string): ts.SourceFile | null {\n const text = ts.sys.readFile(filePath);\n if (text === undefined) return null;\n const scriptKind = filePath.endsWith(\".tsx\")\n ? ts.ScriptKind.TSX\n : ts.ScriptKind.TS;\n // setParentNodes: true — the extractor relies on `.getText()`/`.getStart()`,\n // which walk parent pointers up to the SourceFile.\n return ts.createSourceFile(\n filePath,\n text,\n ts.ScriptTarget.ESNext,\n true,\n scriptKind\n );\n}\n\n/** Resolve compiler options from tsconfig (fallback to sane defaults). */\nfunction resolveCompilerOptions(tsConfigPath?: string): ts.CompilerOptions {\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 return compilerOptions;\n}\n\nfunction extractWithSources(\n sources: ts.SourceFile[],\n checker: ts.TypeChecker | null\n): { results: StepDefinitionMeta[]; needsChecker: boolean } {\n const ctx: ExtractCtx = { checker, needsChecker: false };\n const results: StepDefinitionMeta[] = [];\n for (const sourceFile of sources) {\n results.push(...extractFromSourceFile(sourceFile, ctx));\n }\n return { results, needsChecker: ctx.needsChecker };\n}\n\nfunction extractFromSourceFile(\n sourceFile: ts.SourceFile,\n ctx: ExtractCtx\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, ctx);\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 ctx: ExtractCtx\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, ctx);\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, ctx);\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 ctx: ExtractCtx\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, ctx);\n }\n\n return [];\n}\n\nfunction extractProducedKeysFromCallback(\n callback: ts.ArrowFunction | ts.FunctionExpression,\n ctx: ExtractCtx\n): string[] {\n const body = callback.body;\n\n // Concise arrow: () => ({ key: value })\n if (!ts.isBlock(body)) {\n return extractKeysFromExpression(body, ctx);\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, ctx)) {\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 ctx: ExtractCtx\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 // Non-literal return (a variable, a spread-only object, a function call): the\n // keys can only come from the type checker. On the parse-only pass we have\n // none — flag it so the caller retries with a real Program.\n if (!ctx.checker) {\n ctx.needsChecker = true;\n return [];\n }\n try {\n const type = ctx.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 ctx: ExtractCtx\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 // Tracing the re-exported builder's declaration needs symbol resolution,\n // which only a real Program provides. Flag for the checked retry.\n if (!ctx.checker) {\n ctx.needsChecker = true;\n return null;\n }\n\n const symbol = ctx.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 * as fs from \"node:fs\";\nimport { GherkinClassicTokenMatcher, Parser, AstBuilder } from \"@cucumber/gherkin\";\nimport * as messages from \"@cucumber/messages\";\nimport { ParsedScenario, ParsedStep } from \"./types.js\";\n\ntype GherkinKeyword = \"Given\" | \"When\" | \"Then\" | \"And\" | \"But\";\n\nexport function parseFeatureFiles(filePaths: string[]): ParsedScenario[] {\n const scenarios: ParsedScenario[] = [];\n\n for (const filePath of filePaths) {\n const content = fs.readFileSync(filePath, \"utf-8\");\n const parsed = parseFeatureContent(content, filePath);\n scenarios.push(...parsed);\n }\n\n return scenarios;\n}\n\nexport function parseFeatureContent(\n content: string,\n filePath: string\n): ParsedScenario[] {\n const newId = messages.IdGenerator.uuid();\n const builder = new AstBuilder(newId);\n const matcher = new GherkinClassicTokenMatcher();\n const parser = new Parser(builder, matcher);\n\n const gherkinDocument: messages.GherkinDocument = parser.parse(content);\n const feature = gherkinDocument.feature;\n if (!feature) return [];\n\n // Collect background steps at the feature level\n const featureBackground: messages.Step[] = [];\n const scenarios: ParsedScenario[] = [];\n const featureTags = tagNames(feature.tags);\n\n for (const child of feature.children) {\n if (child.background) {\n featureBackground.push(...child.background.steps);\n }\n\n if (child.scenario) {\n scenarios.push(\n ...expandScenario(\n child.scenario,\n featureBackground,\n filePath,\n featureTags\n )\n );\n }\n\n if (child.rule) {\n // Rules can have their own backgrounds and tags, both inherited by the\n // rule's scenarios.\n const ruleBackground: messages.Step[] = [...featureBackground];\n const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];\n for (const ruleChild of child.rule.children) {\n if (ruleChild.background) {\n ruleBackground.push(...ruleChild.background.steps);\n }\n if (ruleChild.scenario) {\n scenarios.push(\n ...expandScenario(\n ruleChild.scenario,\n ruleBackground,\n filePath,\n ruleTags\n )\n );\n }\n }\n }\n }\n\n return scenarios;\n}\n\n/** Extract tag names (each keeping its leading `@`), deduped in order. */\nfunction tagNames(tags: readonly messages.Tag[] | undefined): string[] {\n return [...new Set((tags ?? []).map(t => t.name))];\n}\n\nfunction expandScenario(\n scenario: messages.Scenario,\n backgroundSteps: messages.Step[],\n filePath: string,\n inheritedTags: string[]\n): ParsedScenario[] {\n const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];\n const hasExamples =\n scenario.examples.length > 0 &&\n scenario.examples.some((e) => e.tableBody.length > 0);\n\n if (!hasExamples) {\n // Regular scenario\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioParsed = convertSteps(scenario.steps);\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);\n\n return [\n {\n name: scenario.name,\n file: filePath,\n line: scenario.location.line,\n steps: allSteps,\n tags: scenarioTags,\n },\n ];\n }\n\n // Scenario Outline — expand with each example row. Rows carry the outline's\n // base name so the runner can group them, plus the Examples-block tags.\n const results: ParsedScenario[] = [];\n for (const example of scenario.examples) {\n if (!example.tableHeader || example.tableBody.length === 0) continue;\n const headers = example.tableHeader.cells.map((c) => c.value);\n const exampleTags = [...scenarioTags, ...tagNames(example.tags)];\n\n for (const row of example.tableBody) {\n const values = row.cells.map((c) => c.value);\n const substitution: Record<string, string> = {};\n headers.forEach((h, i) => {\n substitution[h] = values[i];\n });\n\n const bgParsed = convertSteps(backgroundSteps);\n const scenarioSteps = convertSteps(scenario.steps).map((step) => ({\n ...step,\n text: substituteExampleValues(step.text, substitution),\n }));\n const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);\n\n results.push({\n name: headers.map((h, i) => `${h}=${values[i]}`).join(\", \"),\n file: filePath,\n line: row.location.line,\n steps: allSteps,\n tags: exampleTags,\n outline: { name: scenario.name },\n });\n }\n }\n\n return results;\n}\n\nfunction convertSteps(\n steps: readonly messages.Step[]\n): Omit<ParsedStep, \"effectiveKeyword\">[] {\n return steps.map((step) => ({\n keyword: normalizeKeyword(step.keyword),\n text: step.text,\n line: step.location.line,\n // step.location.column points to the keyword start; shift past the\n // keyword (which includes a trailing space) so column points to the\n // start of the step text. This makes `column + text.length` produce\n // the correct end position for diagnostic ranges.\n column: (step.location.column ?? 1) + step.keyword.length,\n }));\n}\n\nfunction normalizeKeyword(keyword: string): GherkinKeyword {\n const trimmed = keyword.trim();\n // Gherkin keywords may include trailing space, e.g. \"Given \"\n if (trimmed === \"Given\") return \"Given\";\n if (trimmed === \"When\") return \"When\";\n if (trimmed === \"Then\") return \"Then\";\n if (trimmed === \"And\") return \"And\";\n if (trimmed === \"But\") return \"But\";\n // Fallback: treat as Given (shouldn't happen with valid Gherkin)\n return \"Given\";\n}\n\nfunction resolveEffectiveKeywords(\n steps: Omit<ParsedStep, \"effectiveKeyword\">[]\n): ParsedStep[] {\n let lastEffective: \"Given\" | \"When\" | \"Then\" = \"Given\";\n\n return steps.map((step) => {\n let effectiveKeyword: \"Given\" | \"When\" | \"Then\";\n if (step.keyword === \"And\" || step.keyword === \"But\") {\n effectiveKeyword = lastEffective;\n } else {\n effectiveKeyword = step.keyword as \"Given\" | \"When\" | \"Then\";\n }\n lastEffective = effectiveKeyword;\n\n return {\n ...step,\n effectiveKeyword,\n };\n });\n}\n\nfunction substituteExampleValues(\n text: string,\n substitution: Record<string, string>\n): string {\n let result = text;\n for (const [key, value] of Object.entries(substitution)) {\n result = result.replace(new RegExp(`<${key}>`, \"g\"), value);\n }\n return result;\n}\n","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 { globFiles } from \"../globFiles.js\";\nimport { extractStepDefinitions } from \"./stepExtractor.js\";\nimport { parseFeatureFiles, parseFeatureContent } from \"./gherkinParser.js\";\nimport { matchScenarioSteps, findMatchingDefinitions } 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 globFiles(config.stepFiles);\n const featureFilePaths = await globFiles(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"],"mappings":"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"}
|
package/dist/analyzer-cli.js
CHANGED
package/dist/analyzer.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ interface StepDefinitionMeta {
|
|
|
14
14
|
interface ParsedScenario {
|
|
15
15
|
name: string;
|
|
16
16
|
file: string;
|
|
17
|
+
/** 1-based line of the `Scenario:`/`Scenario Outline:` keyword in the file. */
|
|
18
|
+
line?: number;
|
|
17
19
|
steps: ParsedStep[];
|
|
18
20
|
/**
|
|
19
21
|
* Gherkin tags in effect for this scenario, each including the leading `@`
|
package/dist/analyzer.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { n as parseFeatureFiles, t as parseFeatureContent } from "./gherkinParser-Dp2d7JNr.js";
|
|
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-byS8yRrY.js";
|
|
3
2
|
export { analyze, defaultRules, extractStepDefinitions, findMatchingDefinitions, matchScenarioSteps, parseFeatureContent, parseFeatureFiles };
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
const require_gherkinParser = require("./gherkinParser-BT40q_i3.cjs");
|
|
3
|
+
const require_hooks = require("./hooks-Be0cjULN.cjs");
|
|
4
|
+
const require_engine = require("./engine-vqA-eL_T.cjs");
|
|
5
|
+
let node_path = require("node:path");
|
|
6
|
+
node_path = require_gherkinParser.__toESM(node_path, 1);
|
|
7
|
+
let node_url = require("node:url");
|
|
8
|
+
let node_fs_promises = require("node:fs/promises");
|
|
9
|
+
let node_util = require("node:util");
|
|
10
|
+
//#region src/runtime/config.ts
|
|
11
|
+
const DEFAULT_FEATURES = "**/*.feature";
|
|
12
|
+
const DEFAULT_STEPS = "**/*.steps.ts";
|
|
13
|
+
const CONFIG_BASENAMES = [
|
|
14
|
+
"step-forge.config.ts",
|
|
15
|
+
"step-forge.config.mts",
|
|
16
|
+
"step-forge.config.js",
|
|
17
|
+
"step-forge.config.mjs"
|
|
18
|
+
];
|
|
19
|
+
function toArray(value) {
|
|
20
|
+
if (value === void 0) return [];
|
|
21
|
+
return Array.isArray(value) ? value : [value];
|
|
22
|
+
}
|
|
23
|
+
async function exists(p) {
|
|
24
|
+
try {
|
|
25
|
+
await (0, node_fs_promises.access)(p);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Locate and import a `step-forge.config.*` file from `cwd`, returning its
|
|
33
|
+
* default export (or `{}` if none is present). Imported by absolute file URL so
|
|
34
|
+
* Bun transpiles the TypeScript config natively.
|
|
35
|
+
*/
|
|
36
|
+
async function loadConfigFile(cwd) {
|
|
37
|
+
for (const basename of CONFIG_BASENAMES) {
|
|
38
|
+
const candidate = node_path.join(cwd, basename);
|
|
39
|
+
if (!await exists(candidate)) continue;
|
|
40
|
+
const mod = await import((0, node_url.pathToFileURL)(candidate).href);
|
|
41
|
+
return mod.default ?? mod;
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Merge file config with CLI overrides and apply defaults. CLI overrides win
|
|
47
|
+
* field-by-field; array globs are normalised to arrays and default when neither
|
|
48
|
+
* source provides them.
|
|
49
|
+
*/
|
|
50
|
+
function resolveConfig(cwd, file, cli) {
|
|
51
|
+
const pick = (key) => cli[key] ?? file[key];
|
|
52
|
+
const features = toArray(pick("features"));
|
|
53
|
+
const steps = toArray(pick("steps"));
|
|
54
|
+
return {
|
|
55
|
+
cwd,
|
|
56
|
+
features: features.length ? features : [DEFAULT_FEATURES],
|
|
57
|
+
steps: steps.length ? steps : [DEFAULT_STEPS],
|
|
58
|
+
world: pick("world"),
|
|
59
|
+
concurrency: pick("concurrency") ?? 1,
|
|
60
|
+
reporter: pick("reporter") ?? "pretty",
|
|
61
|
+
verbose: pick("verbose") ?? false,
|
|
62
|
+
name: pick("name"),
|
|
63
|
+
tags: pick("tags")
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/runtime/filter.ts
|
|
68
|
+
/** Tokenise a tag expression into atoms, operators, and parentheses. */
|
|
69
|
+
function tokenize(expr) {
|
|
70
|
+
const tokens = [];
|
|
71
|
+
const re = /\s*(\(|\)|@[^\s()]+|\band\b|\bor\b|\bnot\b)\s*/gy;
|
|
72
|
+
let index = 0;
|
|
73
|
+
while (index < expr.length) {
|
|
74
|
+
re.lastIndex = index;
|
|
75
|
+
const m = re.exec(expr);
|
|
76
|
+
if (!m || m.index !== index) throw new Error(`Invalid tag expression near: ${expr.slice(index)}`);
|
|
77
|
+
tokens.push(m[1]);
|
|
78
|
+
index = re.lastIndex;
|
|
79
|
+
}
|
|
80
|
+
return tokens;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compile a tag expression into a predicate via recursive descent
|
|
84
|
+
* (or → and → not → primary). Throws on malformed input so a bad `--tags` flag
|
|
85
|
+
* fails loudly rather than silently matching nothing.
|
|
86
|
+
*/
|
|
87
|
+
function compileTagExpression(expr) {
|
|
88
|
+
const tokens = tokenize(expr);
|
|
89
|
+
let pos = 0;
|
|
90
|
+
const peek = () => tokens[pos];
|
|
91
|
+
const next = () => tokens[pos++];
|
|
92
|
+
const parseOr = () => {
|
|
93
|
+
let left = parseAnd();
|
|
94
|
+
while (peek() === "or") {
|
|
95
|
+
next();
|
|
96
|
+
const right = parseAnd();
|
|
97
|
+
const l = left;
|
|
98
|
+
left = (tags) => l(tags) || right(tags);
|
|
99
|
+
}
|
|
100
|
+
return left;
|
|
101
|
+
};
|
|
102
|
+
const parseAnd = () => {
|
|
103
|
+
let left = parseNot();
|
|
104
|
+
while (peek() === "and") {
|
|
105
|
+
next();
|
|
106
|
+
const right = parseNot();
|
|
107
|
+
const l = left;
|
|
108
|
+
left = (tags) => l(tags) && right(tags);
|
|
109
|
+
}
|
|
110
|
+
return left;
|
|
111
|
+
};
|
|
112
|
+
const parseNot = () => {
|
|
113
|
+
if (peek() === "not") {
|
|
114
|
+
next();
|
|
115
|
+
const operand = parseNot();
|
|
116
|
+
return (tags) => !operand(tags);
|
|
117
|
+
}
|
|
118
|
+
return parsePrimary();
|
|
119
|
+
};
|
|
120
|
+
const parsePrimary = () => {
|
|
121
|
+
const token = next();
|
|
122
|
+
if (token === "(") {
|
|
123
|
+
const inner = parseOr();
|
|
124
|
+
if (next() !== ")") throw new Error("Unbalanced parentheses in tags");
|
|
125
|
+
return inner;
|
|
126
|
+
}
|
|
127
|
+
if (token === void 0 || !token.startsWith("@")) throw new Error(`Expected a tag, got: ${token ?? "end of input"}`);
|
|
128
|
+
return (tags) => tags.includes(token);
|
|
129
|
+
};
|
|
130
|
+
const predicate = parseOr();
|
|
131
|
+
if (pos !== tokens.length) throw new Error(`Unexpected token in tag expression: ${peek()}`);
|
|
132
|
+
return predicate;
|
|
133
|
+
}
|
|
134
|
+
/** Parse a `/pattern/flags` string into a RegExp, else treat it as a substring. */
|
|
135
|
+
function toNameMatcher(name) {
|
|
136
|
+
const delim = /^\/(.*)\/([a-z]*)$/.exec(name);
|
|
137
|
+
if (delim) {
|
|
138
|
+
const re = new RegExp(delim[1], delim[2]);
|
|
139
|
+
return (candidate) => re.test(candidate);
|
|
140
|
+
}
|
|
141
|
+
return (candidate) => candidate.includes(name);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Narrow scenarios by name and tags, then apply `@only` focus. `@skip` is *not*
|
|
145
|
+
* applied here — skipped scenarios stay in the set so the runner can report them
|
|
146
|
+
* as skipped rather than silently dropping them. Returns scenarios in input
|
|
147
|
+
* order.
|
|
148
|
+
*
|
|
149
|
+
* `@only` semantics mirror the Vitest plugin: if any surviving scenario is
|
|
150
|
+
* tagged `@only` (and not `@skip`), the run is focused to just those.
|
|
151
|
+
*/
|
|
152
|
+
function selectScenarios(scenarios, options) {
|
|
153
|
+
let selected = scenarios;
|
|
154
|
+
if (options.name) {
|
|
155
|
+
const matches = toNameMatcher(options.name);
|
|
156
|
+
selected = selected.filter((s) => matches(s.name) || (s.outline ? matches(s.outline.name) : false));
|
|
157
|
+
}
|
|
158
|
+
if (options.tags) {
|
|
159
|
+
const predicate = compileTagExpression(options.tags);
|
|
160
|
+
selected = selected.filter((s) => predicate(s.tags));
|
|
161
|
+
}
|
|
162
|
+
const focused = selected.filter((s) => s.tags.includes("@only") && !s.tags.includes("@skip"));
|
|
163
|
+
return focused.length ? focused : selected;
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/runtime/reporters.ts
|
|
167
|
+
const useColor = !process.env.NO_COLOR && (process.stdout.isTTY ?? false) === true;
|
|
168
|
+
const wrap = (open, close) => (s) => useColor ? `\x1b[${open}m${s}\x1b[${close}m` : s;
|
|
169
|
+
const c = {
|
|
170
|
+
green: wrap(32, 39),
|
|
171
|
+
red: wrap(31, 39),
|
|
172
|
+
yellow: wrap(33, 39),
|
|
173
|
+
dim: wrap(2, 22),
|
|
174
|
+
bold: wrap(1, 22)
|
|
175
|
+
};
|
|
176
|
+
const STATUS_MARK = {
|
|
177
|
+
passed: c.green("✓"),
|
|
178
|
+
failed: c.red("✗"),
|
|
179
|
+
skipped: c.yellow("-")
|
|
180
|
+
};
|
|
181
|
+
/** ✓ / ✗ / - for a whole scenario (skipped = every step skipped). */
|
|
182
|
+
function scenarioMark(result) {
|
|
183
|
+
if (result.status === "failed") return c.red("✗");
|
|
184
|
+
if (result.steps.every((s) => s.status === "skipped")) return c.yellow("-");
|
|
185
|
+
return c.green("✓");
|
|
186
|
+
}
|
|
187
|
+
/** Dot per scenario for the live heartbeat: `.` pass / `F` fail / `-` skip. */
|
|
188
|
+
function scenarioDot(result) {
|
|
189
|
+
if (result.status === "failed") return c.red("F");
|
|
190
|
+
if (result.steps.every((s) => s.status === "skipped")) return c.yellow("-");
|
|
191
|
+
return c.green(".");
|
|
192
|
+
}
|
|
193
|
+
/** Indent every non-empty line of a block by `pad` spaces. */
|
|
194
|
+
function indent(text, pad) {
|
|
195
|
+
const prefix = " ".repeat(pad);
|
|
196
|
+
return text.split("\n").map((line) => line ? prefix + line : line).join("\n");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The error, trimmed to user frames: `Name: message` followed by only the
|
|
200
|
+
* caller's own stack frames (library/engine and `node_modules` frames removed),
|
|
201
|
+
* source-mapped by Bun to the original `.ts`. Falls back to just the message
|
|
202
|
+
* when nothing user-owned is left (e.g. an undefined-step error).
|
|
203
|
+
*/
|
|
204
|
+
function renderError(error, cwd) {
|
|
205
|
+
const header = `${error.name}: ${error.message}`;
|
|
206
|
+
const frames = require_gherkinParser.userFrames(error.stack).map((f) => ` at ${require_gherkinParser.relativeFrame(f, cwd)}`);
|
|
207
|
+
return frames.length ? `${header}\n${frames.join("\n")}` : header;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The detail block shown beneath a failing step: the `.feature` line, the step
|
|
211
|
+
* definition location, and the trimmed error — the two coordinates that make a
|
|
212
|
+
* failure easy to chase (where in the feature, which step definition).
|
|
213
|
+
*/
|
|
214
|
+
function failureDetail(stepResult, result, cwd) {
|
|
215
|
+
const lines = [`feature: ${(0, node_path.relative)(cwd, result.scenario.file)}:${stepResult.step.line}`];
|
|
216
|
+
if (stepResult.source) lines.push(`defined: ${require_gherkinParser.relativeLocation(stepResult.source, cwd)}`);
|
|
217
|
+
if (stepResult.error) lines.push(renderError(stepResult.error, cwd));
|
|
218
|
+
return indent(c.red(lines.join("\n")), 6);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* One scenario as a Cucumber-style block: a marked header with its `.feature`
|
|
222
|
+
* location, then each step with its mark (and, in `verbose`, the step
|
|
223
|
+
* definition location as a trailing comment). A failing step is followed by its
|
|
224
|
+
* failure detail; a scenario-level error (a failing hook, no step to blame) is
|
|
225
|
+
* appended at the end.
|
|
226
|
+
*/
|
|
227
|
+
function renderScenario(result, cwd, opts) {
|
|
228
|
+
const label = result.scenario.outline ? `${result.scenario.outline.name} › ${result.scenario.name}` : result.scenario.name;
|
|
229
|
+
const lines = [`${scenarioMark(result)} ${c.bold(label)}`];
|
|
230
|
+
const loc = result.scenario.line ? `${(0, node_path.relative)(cwd, result.scenario.file)}:${result.scenario.line}` : (0, node_path.relative)(cwd, result.scenario.file);
|
|
231
|
+
lines.push(indent(c.dim(loc), 4));
|
|
232
|
+
lines.push("");
|
|
233
|
+
for (const step of result.scenario.steps) {
|
|
234
|
+
const sr = result.steps.find((s) => s.step === step);
|
|
235
|
+
const status = sr?.status ?? "skipped";
|
|
236
|
+
const comment = opts.stepSource && sr?.source ? c.dim(` # ${require_gherkinParser.relativeLocation(sr.source, cwd)}`) : "";
|
|
237
|
+
lines.push(` ${STATUS_MARK[status]} ${c.dim(step.effectiveKeyword)} ${step.text}${comment}`);
|
|
238
|
+
if (sr?.status === "failed") lines.push(failureDetail(sr, result, cwd));
|
|
239
|
+
}
|
|
240
|
+
const stepFailed = result.steps.some((s) => s.status === "failed");
|
|
241
|
+
if (result.error && !stepFailed) lines.push(indent(c.red(renderError(result.error, cwd)), 4));
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
function tally(results) {
|
|
245
|
+
const totals = {
|
|
246
|
+
scenarios: {
|
|
247
|
+
passed: 0,
|
|
248
|
+
failed: 0,
|
|
249
|
+
skipped: 0
|
|
250
|
+
},
|
|
251
|
+
steps: {
|
|
252
|
+
passed: 0,
|
|
253
|
+
failed: 0,
|
|
254
|
+
skipped: 0
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
for (const result of results) {
|
|
258
|
+
if (result.status === "failed") totals.scenarios.failed++;
|
|
259
|
+
else if (result.steps.every((s) => s.status === "skipped")) totals.scenarios.skipped++;
|
|
260
|
+
else totals.scenarios.passed++;
|
|
261
|
+
for (const step of result.steps) totals.steps[step.status]++;
|
|
262
|
+
}
|
|
263
|
+
return totals;
|
|
264
|
+
}
|
|
265
|
+
function renderSummary(results, durationMs) {
|
|
266
|
+
const t = tally(results);
|
|
267
|
+
const scenarioTotal = t.scenarios.passed + t.scenarios.failed + t.scenarios.skipped;
|
|
268
|
+
const stepTotal = t.steps.passed + t.steps.failed + t.steps.skipped;
|
|
269
|
+
const part = (n, label, color) => n > 0 ? color(`${n} ${label}`) : null;
|
|
270
|
+
const line = (total, noun, counts) => {
|
|
271
|
+
const parts = [
|
|
272
|
+
part(counts.passed, "passed", c.green),
|
|
273
|
+
part(counts.failed, "failed", c.red),
|
|
274
|
+
part(counts.skipped, "skipped", c.yellow)
|
|
275
|
+
].filter((x) => x !== null);
|
|
276
|
+
return `${total} ${noun}${total === 1 ? "" : "s"} (${parts.join(", ")})`;
|
|
277
|
+
};
|
|
278
|
+
return [
|
|
279
|
+
line(scenarioTotal, "scenario", t.scenarios),
|
|
280
|
+
line(stepTotal, "step", t.steps),
|
|
281
|
+
c.dim(`${(durationMs / 1e3).toFixed(2)}s`)
|
|
282
|
+
].join("\n");
|
|
283
|
+
}
|
|
284
|
+
function write(text) {
|
|
285
|
+
process.stdout.write(text);
|
|
286
|
+
}
|
|
287
|
+
/** Feature-grouped tree of every scenario (used by `--verbose`). */
|
|
288
|
+
function renderFullTree(results, cwd) {
|
|
289
|
+
const groups = /* @__PURE__ */ new Map();
|
|
290
|
+
for (const r of results) {
|
|
291
|
+
const bucket = groups.get(r.scenario.file) ?? [];
|
|
292
|
+
bucket.push(r);
|
|
293
|
+
groups.set(r.scenario.file, bucket);
|
|
294
|
+
}
|
|
295
|
+
const out = [];
|
|
296
|
+
for (const [file, group] of groups) {
|
|
297
|
+
out.push(c.bold(`Feature: ${(0, node_path.relative)(cwd, file)}`), "");
|
|
298
|
+
for (const r of group) out.push(indent(renderScenario(r, cwd, { stepSource: true }), 2), "");
|
|
299
|
+
}
|
|
300
|
+
return out.join("\n");
|
|
301
|
+
}
|
|
302
|
+
/** The failing scenarios only (used by the default, non-verbose output). */
|
|
303
|
+
function renderFailures(results, cwd) {
|
|
304
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
305
|
+
if (!failed.length) return "";
|
|
306
|
+
return failed.map((r) => renderScenario(r, cwd, { stepSource: false })).join("\n\n") + "\n\n";
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Default reporter. Non-verbose: a dot per scenario as a heartbeat, then the
|
|
310
|
+
* failing scenarios in Cucumber-style detail, then the summary. Verbose: the
|
|
311
|
+
* full feature → scenario → step tree instead of just failures.
|
|
312
|
+
*/
|
|
313
|
+
function prettyReporter(opts = {}) {
|
|
314
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
315
|
+
const verbose = opts.verbose ?? false;
|
|
316
|
+
return {
|
|
317
|
+
onScenarioEnd(result) {
|
|
318
|
+
if (!verbose) write(scenarioDot(result));
|
|
319
|
+
},
|
|
320
|
+
onComplete(results, durationMs) {
|
|
321
|
+
write(`${verbose ? `\n${renderFullTree(results, cwd)}\n` : `\n\n${renderFailures(results, cwd)}`}${renderSummary(results, durationMs)}\n`);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Compact reporter: a dot per scenario, then failures in detail and the
|
|
327
|
+
* summary. Always minimal — `verbose` is accepted for interface parity but does
|
|
328
|
+
* not expand the output (use `pretty --verbose` for the full tree).
|
|
329
|
+
*/
|
|
330
|
+
function progressReporter(opts = {}) {
|
|
331
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
332
|
+
return {
|
|
333
|
+
onScenarioEnd(result) {
|
|
334
|
+
write(scenarioDot(result));
|
|
335
|
+
},
|
|
336
|
+
onComplete(results, durationMs) {
|
|
337
|
+
write(`\n\n${renderFailures(results, cwd)}${renderSummary(results, durationMs)}\n`);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
function makeReporter(name, opts = {}) {
|
|
342
|
+
return name === "progress" ? progressReporter(opts) : prettyReporter(opts);
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/runtime/runner.ts
|
|
346
|
+
/**
|
|
347
|
+
* Import every step-definition module so its `.step(...)` calls self-register
|
|
348
|
+
* into the shared `globalRegistry`. Imported by file URL so Bun transpiles the
|
|
349
|
+
* TypeScript natively.
|
|
350
|
+
*/
|
|
351
|
+
async function importSteps(files) {
|
|
352
|
+
for (const file of files) await import((0, node_url.pathToFileURL)(file).href);
|
|
353
|
+
}
|
|
354
|
+
/** Load the configured world factory, or default to a fresh `BasicWorld`. */
|
|
355
|
+
async function loadWorldFactory(world, cwd) {
|
|
356
|
+
if (!world) return () => new require_gherkinParser.BasicWorld();
|
|
357
|
+
const mod = await import((0, node_url.pathToFileURL)(node_path.resolve(cwd, world)).href);
|
|
358
|
+
const factory = mod.default ?? mod;
|
|
359
|
+
if (typeof factory !== "function") throw new Error(`World module ${world} must default-export a factory function () => world`);
|
|
360
|
+
return factory;
|
|
361
|
+
}
|
|
362
|
+
/** A scenario carrying `@skip` never runs: report it with all steps skipped. */
|
|
363
|
+
function skippedResult(scenario) {
|
|
364
|
+
return {
|
|
365
|
+
scenario,
|
|
366
|
+
status: "passed",
|
|
367
|
+
steps: scenario.steps.map((step) => ({
|
|
368
|
+
step,
|
|
369
|
+
status: "skipped"
|
|
370
|
+
}))
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Run `items` through `worker`, at most `limit` in flight. Results come back in
|
|
375
|
+
* input order even though completion order is not deterministic. A minimal
|
|
376
|
+
* dependency-free pool — the whole point of the single-process model.
|
|
377
|
+
*/
|
|
378
|
+
async function runPool(items, limit, worker) {
|
|
379
|
+
const results = new Array(items.length);
|
|
380
|
+
let cursor = 0;
|
|
381
|
+
const runNext = async () => {
|
|
382
|
+
while (cursor < items.length) {
|
|
383
|
+
const index = cursor++;
|
|
384
|
+
results[index] = await worker(items[index], index);
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, runNext);
|
|
388
|
+
await Promise.all(workers);
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Execute a whole run end to end: discover and import steps, load the world,
|
|
393
|
+
* parse and select scenarios, then run them concurrently against a
|
|
394
|
+
* compiled-once step table with global/feature hooks around the batch. Never
|
|
395
|
+
* throws for test failures — inspect `RunResult.passed`.
|
|
396
|
+
*/
|
|
397
|
+
async function run(config, reporter = makeReporter(config.reporter, {
|
|
398
|
+
cwd: config.cwd,
|
|
399
|
+
verbose: config.verbose
|
|
400
|
+
})) {
|
|
401
|
+
const start = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
402
|
+
const [featureFiles, stepFiles] = await Promise.all([require_gherkinParser.globFiles(config.features, config.cwd), require_gherkinParser.globFiles(config.steps, config.cwd)]);
|
|
403
|
+
await importSteps(stepFiles);
|
|
404
|
+
const makeWorld = await loadWorldFactory(config.world, config.cwd);
|
|
405
|
+
const compiled = require_engine.compileRegistry(require_hooks.globalRegistry);
|
|
406
|
+
const selected = selectScenarios(require_gherkinParser.parseFeatureFiles(featureFiles), {
|
|
407
|
+
name: config.name,
|
|
408
|
+
tags: config.tags
|
|
409
|
+
});
|
|
410
|
+
await require_hooks.runHooksParallel("global", "before", require_hooks.globalHookRegistry);
|
|
411
|
+
await require_hooks.runHooks("feature", "before", require_hooks.globalHookRegistry);
|
|
412
|
+
const results = await runPool(selected, config.concurrency, async (scenario) => {
|
|
413
|
+
const result = scenario.tags.includes("@skip") ? skippedResult(scenario) : await require_engine.runScenario(scenario, compiled, makeWorld, require_hooks.globalHookRegistry);
|
|
414
|
+
reporter.onScenarioEnd?.(result);
|
|
415
|
+
return result;
|
|
416
|
+
});
|
|
417
|
+
await require_hooks.runHooks("feature", "after", require_hooks.globalHookRegistry);
|
|
418
|
+
await require_hooks.runHooksParallel("global", "after", require_hooks.globalHookRegistry);
|
|
419
|
+
const durationMs = (typeof performance !== "undefined" ? performance.now() : Date.now()) - start;
|
|
420
|
+
reporter.onComplete(results, durationMs);
|
|
421
|
+
return {
|
|
422
|
+
results,
|
|
423
|
+
passed: results.every((r) => r.status !== "failed"),
|
|
424
|
+
durationMs
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
//#endregion
|
|
428
|
+
//#region src/runtime/cli.ts
|
|
429
|
+
const HELP = `step-forge — native TypeScript runner for Gherkin step definitions
|
|
430
|
+
|
|
431
|
+
Usage:
|
|
432
|
+
step-forge [options] [feature globs...]
|
|
433
|
+
|
|
434
|
+
Options:
|
|
435
|
+
-t, --tags <expr> Tag expression, e.g. "@smoke and not @wip"
|
|
436
|
+
-n, --name <pattern> Only scenarios whose name matches (substring or /regex/)
|
|
437
|
+
-s, --steps <glob> Step-definition module glob (repeatable)
|
|
438
|
+
-w, --world <module> World factory module (default export () => world)
|
|
439
|
+
-c, --concurrency <n> Max scenarios in flight (default: 1, i.e. serial)
|
|
440
|
+
-r, --reporter <name> "pretty" (default) or "progress"
|
|
441
|
+
-v, --verbose Report every scenario, not just failures
|
|
442
|
+
--config <path> Config file directory (default: cwd)
|
|
443
|
+
-h, --help Show this help
|
|
444
|
+
|
|
445
|
+
Positional arguments are feature globs and override the configured features.
|
|
446
|
+
`;
|
|
447
|
+
/** Parse argv into config overrides. Positional args become feature globs. */
|
|
448
|
+
function parseCli(argv) {
|
|
449
|
+
const { values, positionals } = (0, node_util.parseArgs)({
|
|
450
|
+
args: argv,
|
|
451
|
+
allowPositionals: true,
|
|
452
|
+
options: {
|
|
453
|
+
tags: {
|
|
454
|
+
type: "string",
|
|
455
|
+
short: "t"
|
|
456
|
+
},
|
|
457
|
+
name: {
|
|
458
|
+
type: "string",
|
|
459
|
+
short: "n"
|
|
460
|
+
},
|
|
461
|
+
steps: {
|
|
462
|
+
type: "string",
|
|
463
|
+
short: "s",
|
|
464
|
+
multiple: true
|
|
465
|
+
},
|
|
466
|
+
world: {
|
|
467
|
+
type: "string",
|
|
468
|
+
short: "w"
|
|
469
|
+
},
|
|
470
|
+
concurrency: {
|
|
471
|
+
type: "string",
|
|
472
|
+
short: "c"
|
|
473
|
+
},
|
|
474
|
+
reporter: {
|
|
475
|
+
type: "string",
|
|
476
|
+
short: "r"
|
|
477
|
+
},
|
|
478
|
+
verbose: {
|
|
479
|
+
type: "boolean",
|
|
480
|
+
short: "v"
|
|
481
|
+
},
|
|
482
|
+
config: { type: "string" },
|
|
483
|
+
help: {
|
|
484
|
+
type: "boolean",
|
|
485
|
+
short: "h"
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
if (values.help) {
|
|
490
|
+
process.stdout.write(HELP);
|
|
491
|
+
process.exit(0);
|
|
492
|
+
}
|
|
493
|
+
const overrides = {};
|
|
494
|
+
if (positionals.length) overrides.features = positionals;
|
|
495
|
+
if (values.tags) overrides.tags = values.tags;
|
|
496
|
+
if (values.name) overrides.name = values.name;
|
|
497
|
+
if (values.steps) overrides.steps = values.steps;
|
|
498
|
+
if (values.world) overrides.world = values.world;
|
|
499
|
+
if (values.verbose) overrides.verbose = true;
|
|
500
|
+
if (values.reporter) {
|
|
501
|
+
if (values.reporter !== "pretty" && values.reporter !== "progress") throw new Error(`Unknown reporter: ${values.reporter}`);
|
|
502
|
+
overrides.reporter = values.reporter;
|
|
503
|
+
}
|
|
504
|
+
if (values.concurrency !== void 0) {
|
|
505
|
+
const n = Number(values.concurrency);
|
|
506
|
+
if (!Number.isInteger(n) || n < 1) throw new Error(`--concurrency must be a positive integer`);
|
|
507
|
+
overrides.concurrency = n;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
cwd: values.config ? node_path.resolve(process.cwd(), values.config) : process.cwd(),
|
|
511
|
+
overrides
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function main() {
|
|
515
|
+
const { cwd, overrides } = parseCli(process.argv.slice(2));
|
|
516
|
+
const { passed } = await run(resolveConfig(cwd, await loadConfigFile(cwd), overrides));
|
|
517
|
+
process.exitCode = passed ? 0 : 1;
|
|
518
|
+
}
|
|
519
|
+
main().catch((err) => {
|
|
520
|
+
process.stderr.write(`${err instanceof Error ? err.stack : String(err)}\n`);
|
|
521
|
+
process.exitCode = 1;
|
|
522
|
+
});
|
|
523
|
+
//#endregion
|
|
524
|
+
|
|
525
|
+
//# sourceMappingURL=cli.cjs.map
|