@step-forge/step-forge 0.0.20 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +2 -0
  2. package/RUNTIME.md +242 -0
  3. package/dist/{analyzer-DJyJbU_V.js → analyzer-byS8yRrY.js} +202 -34
  4. package/dist/analyzer-byS8yRrY.js.map +1 -0
  5. package/dist/analyzer-cli.js +1 -1
  6. package/dist/analyzer.d.ts +2 -0
  7. package/dist/analyzer.js +1 -2
  8. package/dist/cli.cjs +525 -0
  9. package/dist/cli.cjs.map +1 -0
  10. package/dist/cli.d.cts +1 -0
  11. package/dist/cli.d.ts +1 -0
  12. package/dist/cli.js +526 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/{hooks-Dar49TtT.d.ts → config-C7PCYgYy.d.cts} +65 -16
  15. package/dist/{hooks-Dar49TtT.d.cts → config-C7PCYgYy.d.ts} +65 -16
  16. package/dist/engine-DPVLEHBi.js +163 -0
  17. package/dist/engine-DPVLEHBi.js.map +1 -0
  18. package/dist/engine-vqA-eL_T.cjs +186 -0
  19. package/dist/engine-vqA-eL_T.cjs.map +1 -0
  20. package/dist/gherkinParser-BT40q_i3.cjs +338 -0
  21. package/dist/gherkinParser-BT40q_i3.cjs.map +1 -0
  22. package/dist/gherkinParser-NcttZgN4.js +259 -0
  23. package/dist/gherkinParser-NcttZgN4.js.map +1 -0
  24. package/dist/hooks-BDCMKeNq.js +71 -0
  25. package/dist/{hooks-CywugMQQ.js.map → hooks-BDCMKeNq.js.map} +1 -1
  26. package/dist/{hooks-CGYzwDOv.cjs → hooks-Be0cjULN.cjs} +20 -31
  27. package/dist/{hooks-CGYzwDOv.cjs.map → hooks-Be0cjULN.cjs.map} +1 -1
  28. package/dist/runtime.cjs +7 -162
  29. package/dist/runtime.d.cts +44 -8
  30. package/dist/runtime.d.ts +44 -8
  31. package/dist/runtime.js +3 -159
  32. package/dist/step-forge.cjs +73 -216
  33. package/dist/step-forge.cjs.map +1 -1
  34. package/dist/step-forge.d.cts +19 -10
  35. package/dist/step-forge.d.ts +19 -10
  36. package/dist/step-forge.js +67 -185
  37. package/dist/step-forge.js.map +1 -1
  38. package/package.json +12 -18
  39. package/dist/analyzer-DJyJbU_V.js.map +0 -1
  40. package/dist/gherkinParser-Dp2d7JNr.js +0 -116
  41. package/dist/gherkinParser-Dp2d7JNr.js.map +0 -1
  42. package/dist/hooks-CywugMQQ.js +0 -82
  43. package/dist/runtime.cjs.map +0 -1
  44. package/dist/runtime.js.map +0 -1
  45. package/dist/vitest.d.ts +0 -74
  46. package/dist/vitest.js +0 -136
  47. package/dist/vitest.js.map +0 -1
@@ -1,11 +1,7 @@
1
- import { o as globalRegistry, r as globalHookRegistry } from "./hooks-CywugMQQ.js";
1
+ import { n as globalHookRegistry, o as globalRegistry } from "./hooks-BDCMKeNq.js";
2
+ import { a as captureDefinitionSite, i as BasicWorld, n as parseFeatureFiles, r as globFiles, t as parseFeatureContent } from "./gherkinParser-NcttZgN4.js";
2
3
  import _ from "lodash";
3
- import { glob } from "node:fs/promises";
4
- import * as path from "node:path";
5
4
  import ts from "typescript";
6
- import * as fs from "node:fs";
7
- import { AstBuilder, GherkinClassicTokenMatcher, Parser } from "@cucumber/gherkin";
8
- import * as messages from "@cucumber/messages";
9
5
  //#region src/builderTypeUtils.ts
10
6
  const isString = (statement) => typeof statement === "string";
11
7
  //#endregion
@@ -118,11 +114,13 @@ const addStep = (statement, stepType, dependencies = {
118
114
  });
119
115
  world[stepType].merge({ ...result });
120
116
  };
117
+ const source = captureDefinitionSite();
121
118
  globalRegistry.add({
122
119
  stepType,
123
120
  expression,
124
121
  parsers,
125
- execute
122
+ execute,
123
+ source
126
124
  });
127
125
  return {
128
126
  statement,
@@ -196,42 +194,6 @@ const thenStatement = () => (statement) => {
196
194
  };
197
195
  const thenBuilder = () => ({ statement: thenStatement() });
198
196
  //#endregion
199
- //#region src/world.ts
200
- function mergeCustomizer(objValue, srcValue) {
201
- if (_.isArray(objValue)) return objValue.concat(srcValue);
202
- else if (objValue && !_.isPlainObject(objValue) && objValue !== srcValue) throw new Error(`Merge would have destroyed previous value ${objValue} with ${srcValue}`);
203
- return objValue;
204
- }
205
- var BasicWorld = class {
206
- givenState = {};
207
- whenState = {};
208
- thenState = {};
209
- get given() {
210
- return {
211
- ...this.givenState,
212
- merge: (newState) => {
213
- this.givenState = _.merge({ ...this.givenState }, newState, mergeCustomizer);
214
- }
215
- };
216
- }
217
- get when() {
218
- return {
219
- ...this.whenState,
220
- merge: (newState) => {
221
- this.whenState = _.merge({ ...this.whenState }, newState, mergeCustomizer);
222
- }
223
- };
224
- }
225
- get then() {
226
- return {
227
- ...this.thenState,
228
- merge: (newState) => {
229
- this.thenState = _.merge({ ...this.thenState }, newState, mergeCustomizer);
230
- }
231
- };
232
- }
233
- };
234
- //#endregion
235
197
  //#region src/analyzer/stepExtractor.ts
236
198
  const BUILDER_NAMES = {
237
199
  givenBuilder: "given",
@@ -239,6 +201,21 @@ const BUILDER_NAMES = {
239
201
  thenBuilder: "then"
240
202
  };
241
203
  function extractStepDefinitions(filePaths, tsConfigPath) {
204
+ const fast = extractWithSources(filePaths.map(parseSourceFile).filter((s) => s !== null), null);
205
+ if (!fast.needsChecker) return fast.results;
206
+ const program = ts.createProgram(filePaths, resolveCompilerOptions(tsConfigPath));
207
+ const checker = program.getTypeChecker();
208
+ return extractWithSources(filePaths.map((fp) => program.getSourceFile(fp)).filter((s) => s !== void 0), checker).results;
209
+ }
210
+ /** Parse a single file into an AST with no type resolution. */
211
+ function parseSourceFile(filePath) {
212
+ const text = ts.sys.readFile(filePath);
213
+ if (text === void 0) return null;
214
+ const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
215
+ return ts.createSourceFile(filePath, text, ts.ScriptTarget.ESNext, true, scriptKind);
216
+ }
217
+ /** Resolve compiler options from tsconfig (fallback to sane defaults). */
218
+ function resolveCompilerOptions(tsConfigPath) {
242
219
  const configPath = tsConfigPath ?? ts.findConfigFile(process.cwd(), ts.sys.fileExists);
243
220
  let compilerOptions = {
244
221
  target: ts.ScriptTarget.ESNext,
@@ -252,22 +229,25 @@ function extractStepDefinitions(filePaths, tsConfigPath) {
252
229
  if (!configFile.error) compilerOptions = ts.parseJsonConfigFileContent(configFile.config, ts.sys, configPath.replace(/[/\\][^/\\]+$/, "")).options;
253
230
  }
254
231
  compilerOptions.noEmit = true;
255
- const program = ts.createProgram(filePaths, compilerOptions);
256
- const checker = program.getTypeChecker();
232
+ return compilerOptions;
233
+ }
234
+ function extractWithSources(sources, checker) {
235
+ const ctx = {
236
+ checker,
237
+ needsChecker: false
238
+ };
257
239
  const results = [];
258
- for (const filePath of filePaths) {
259
- const sourceFile = program.getSourceFile(filePath);
260
- if (!sourceFile) continue;
261
- const fileResults = extractFromSourceFile(sourceFile, checker);
262
- results.push(...fileResults);
263
- }
264
- return results;
240
+ for (const sourceFile of sources) results.push(...extractFromSourceFile(sourceFile, ctx));
241
+ return {
242
+ results,
243
+ needsChecker: ctx.needsChecker
244
+ };
265
245
  }
266
- function extractFromSourceFile(sourceFile, checker) {
246
+ function extractFromSourceFile(sourceFile, ctx) {
267
247
  const results = [];
268
248
  function visit(node) {
269
249
  if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "step") {
270
- const meta = extractFromRegisterCall(node, sourceFile, checker);
250
+ const meta = extractFromRegisterCall(node, sourceFile, ctx);
271
251
  if (meta) results.push(meta);
272
252
  }
273
253
  ts.forEachChild(node, visit);
@@ -275,7 +255,7 @@ function extractFromSourceFile(sourceFile, checker) {
275
255
  visit(sourceFile);
276
256
  return results;
277
257
  }
278
- function extractFromRegisterCall(registerCall, sourceFile, checker) {
258
+ function extractFromRegisterCall(registerCall, sourceFile, ctx) {
279
259
  const chain = collectCallChain(registerCall);
280
260
  let stepType = null;
281
261
  let expression = null;
@@ -290,7 +270,7 @@ function extractFromRegisterCall(registerCall, sourceFile, checker) {
290
270
  if (!name) continue;
291
271
  if (name === "register") continue;
292
272
  if (name === "step") {
293
- produces = extractProducedKeys(link, checker);
273
+ produces = extractProducedKeys(link, ctx);
294
274
  continue;
295
275
  }
296
276
  if (name === "dependencies") {
@@ -307,7 +287,7 @@ function extractFromRegisterCall(registerCall, sourceFile, checker) {
307
287
  }
308
288
  }
309
289
  if (!stepType || !expression) {
310
- const reExport = resolveReExportedCall(chain, checker);
290
+ const reExport = resolveReExportedCall(chain, ctx);
311
291
  if (reExport) {
312
292
  if (!stepType) stepType = reExport.stepType;
313
293
  if (!expression) expression = reExport.expression;
@@ -394,33 +374,37 @@ function extractDependencies(depsCall) {
394
374
  }
395
375
  return deps;
396
376
  }
397
- function extractProducedKeys(stepCall, checker) {
377
+ function extractProducedKeys(stepCall, ctx) {
398
378
  const callback = stepCall.arguments[0];
399
379
  if (!callback) return [];
400
- if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, checker);
380
+ if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) return extractProducedKeysFromCallback(callback, ctx);
401
381
  return [];
402
382
  }
403
- function extractProducedKeysFromCallback(callback, checker) {
383
+ function extractProducedKeysFromCallback(callback, ctx) {
404
384
  const body = callback.body;
405
- if (!ts.isBlock(body)) return extractKeysFromExpression(body, checker);
385
+ if (!ts.isBlock(body)) return extractKeysFromExpression(body, ctx);
406
386
  const keys = /* @__PURE__ */ new Set();
407
387
  function visitReturn(node) {
408
- if (ts.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, checker)) keys.add(key);
388
+ if (ts.isReturnStatement(node) && node.expression) for (const key of extractKeysFromExpression(node.expression, ctx)) keys.add(key);
409
389
  ts.forEachChild(node, visitReturn);
410
390
  }
411
391
  visitReturn(body);
412
392
  return [...keys];
413
393
  }
414
- function extractKeysFromExpression(expr, checker) {
394
+ function extractKeysFromExpression(expr, ctx) {
415
395
  while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
416
396
  if (ts.isObjectLiteralExpression(expr)) return expr.properties.filter((p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)).map((p) => p.name.getText()).filter(Boolean);
397
+ if (!ctx.checker) {
398
+ ctx.needsChecker = true;
399
+ return [];
400
+ }
417
401
  try {
418
- return checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
402
+ return ctx.checker.getTypeAtLocation(expr).getProperties().map((p) => p.name).filter((n) => n !== "merge");
419
403
  } catch {
420
404
  return [];
421
405
  }
422
406
  }
423
- function resolveReExportedCall(chain, checker) {
407
+ function resolveReExportedCall(chain, ctx) {
424
408
  const lastCall = chain[chain.length - 1];
425
409
  if (!lastCall) return null;
426
410
  const expr = lastCall.expression;
@@ -428,7 +412,11 @@ function resolveReExportedCall(chain, checker) {
428
412
  if (ts.isIdentifier(expr)) identifier = expr;
429
413
  else if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression)) identifier = expr.expression;
430
414
  if (!identifier) return null;
431
- const symbol = checker.getSymbolAtLocation(identifier);
415
+ if (!ctx.checker) {
416
+ ctx.needsChecker = true;
417
+ return null;
418
+ }
419
+ const symbol = ctx.checker.getSymbolAtLocation(identifier);
432
420
  if (!symbol) return null;
433
421
  const decl = symbol.valueDeclaration;
434
422
  if (!decl || !ts.isVariableDeclaration(decl) || !decl.initializer) return null;
@@ -449,116 +437,6 @@ function resolveReExportedCall(chain, checker) {
449
437
  return null;
450
438
  }
451
439
  //#endregion
452
- //#region src/analyzer/gherkinParser.ts
453
- function parseFeatureFiles(filePaths) {
454
- const scenarios = [];
455
- for (const filePath of filePaths) {
456
- const parsed = parseFeatureContent(fs.readFileSync(filePath, "utf-8"), filePath);
457
- scenarios.push(...parsed);
458
- }
459
- return scenarios;
460
- }
461
- function parseFeatureContent(content, filePath) {
462
- const feature = new Parser(new AstBuilder(messages.IdGenerator.uuid()), new GherkinClassicTokenMatcher()).parse(content).feature;
463
- if (!feature) return [];
464
- const featureBackground = [];
465
- const scenarios = [];
466
- const featureTags = tagNames(feature.tags);
467
- for (const child of feature.children) {
468
- if (child.background) featureBackground.push(...child.background.steps);
469
- if (child.scenario) scenarios.push(...expandScenario(child.scenario, featureBackground, filePath, featureTags));
470
- if (child.rule) {
471
- const ruleBackground = [...featureBackground];
472
- const ruleTags = [...featureTags, ...tagNames(child.rule.tags)];
473
- for (const ruleChild of child.rule.children) {
474
- if (ruleChild.background) ruleBackground.push(...ruleChild.background.steps);
475
- if (ruleChild.scenario) scenarios.push(...expandScenario(ruleChild.scenario, ruleBackground, filePath, ruleTags));
476
- }
477
- }
478
- }
479
- return scenarios;
480
- }
481
- /** Extract tag names (each keeping its leading `@`), deduped in order. */
482
- function tagNames(tags) {
483
- return [...new Set((tags ?? []).map((t) => t.name))];
484
- }
485
- function expandScenario(scenario, backgroundSteps, filePath, inheritedTags) {
486
- const scenarioTags = [...inheritedTags, ...tagNames(scenario.tags)];
487
- if (!(scenario.examples.length > 0 && scenario.examples.some((e) => e.tableBody.length > 0))) {
488
- const bgParsed = convertSteps(backgroundSteps);
489
- const scenarioParsed = convertSteps(scenario.steps);
490
- const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioParsed]);
491
- return [{
492
- name: scenario.name,
493
- file: filePath,
494
- steps: allSteps,
495
- tags: scenarioTags
496
- }];
497
- }
498
- const results = [];
499
- for (const example of scenario.examples) {
500
- if (!example.tableHeader || example.tableBody.length === 0) continue;
501
- const headers = example.tableHeader.cells.map((c) => c.value);
502
- const exampleTags = [...scenarioTags, ...tagNames(example.tags)];
503
- for (const row of example.tableBody) {
504
- const values = row.cells.map((c) => c.value);
505
- const substitution = {};
506
- headers.forEach((h, i) => {
507
- substitution[h] = values[i];
508
- });
509
- const bgParsed = convertSteps(backgroundSteps);
510
- const scenarioSteps = convertSteps(scenario.steps).map((step) => ({
511
- ...step,
512
- text: substituteExampleValues(step.text, substitution)
513
- }));
514
- const allSteps = resolveEffectiveKeywords([...bgParsed, ...scenarioSteps]);
515
- results.push({
516
- name: headers.map((h, i) => `${h}=${values[i]}`).join(", "),
517
- file: filePath,
518
- steps: allSteps,
519
- tags: exampleTags,
520
- outline: { name: scenario.name }
521
- });
522
- }
523
- }
524
- return results;
525
- }
526
- function convertSteps(steps) {
527
- return steps.map((step) => ({
528
- keyword: normalizeKeyword(step.keyword),
529
- text: step.text,
530
- line: step.location.line,
531
- column: (step.location.column ?? 1) + step.keyword.length
532
- }));
533
- }
534
- function normalizeKeyword(keyword) {
535
- const trimmed = keyword.trim();
536
- if (trimmed === "Given") return "Given";
537
- if (trimmed === "When") return "When";
538
- if (trimmed === "Then") return "Then";
539
- if (trimmed === "And") return "And";
540
- if (trimmed === "But") return "But";
541
- return "Given";
542
- }
543
- function resolveEffectiveKeywords(steps) {
544
- let lastEffective = "Given";
545
- return steps.map((step) => {
546
- let effectiveKeyword;
547
- if (step.keyword === "And" || step.keyword === "But") effectiveKeyword = lastEffective;
548
- else effectiveKeyword = step.keyword;
549
- lastEffective = effectiveKeyword;
550
- return {
551
- ...step,
552
- effectiveKeyword
553
- };
554
- });
555
- }
556
- function substituteExampleValues(text, substitution) {
557
- let result = text;
558
- for (const [key, value] of Object.entries(substitution)) result = result.replace(new RegExp(`<${key}>`, "g"), value);
559
- return result;
560
- }
561
- //#endregion
562
440
  //#region src/analyzer/stepMatcher.ts
563
441
  function compileDefinitions(definitions) {
564
442
  const compiled = [];
@@ -693,8 +571,8 @@ function runRules(rules, scenario, matchedSteps) {
693
571
  //#region src/analyzer/index.ts
694
572
  async function analyze(config, options) {
695
573
  const rules = options?.rules ?? defaultRules;
696
- const stepFilePaths = await resolveGlobs(config.stepFiles);
697
- const featureFilePaths = await resolveGlobs(config.featureFiles);
574
+ const stepFilePaths = await globFiles(config.stepFiles);
575
+ const featureFilePaths = await globFiles(config.featureFiles);
698
576
  if (stepFilePaths.length === 0) return [];
699
577
  if (featureFilePaths.length === 0) return [];
700
578
  const stepDefinitions = extractStepDefinitions(stepFilePaths, config.tsConfigPath);
@@ -706,11 +584,6 @@ async function analyze(config, options) {
706
584
  }
707
585
  return diagnostics;
708
586
  }
709
- async function resolveGlobs(patterns) {
710
- const files = [];
711
- for (const pattern of patterns) for await (const file of glob(pattern)) files.push(path.resolve(file));
712
- return [...new Set(files)];
713
- }
714
587
  //#endregion
715
588
  //#region src/init.ts
716
589
  const createBuilders = () => {
@@ -763,7 +636,12 @@ function afterFeature(fn) {
763
636
  fn
764
637
  });
765
638
  }
766
- /** Run once before the entire test run, across all feature files. */
639
+ /**
640
+ * Run once before the entire run, ahead of any scenario (and before
641
+ * concurrency starts). Multiple `beforeAll` hooks run **in parallel** with no
642
+ * ordering between them — if a step of setup must precede another, sequence both
643
+ * inside a single hook.
644
+ */
767
645
  function beforeAll(fn) {
768
646
  globalHookRegistry.add({
769
647
  scope: "global",
@@ -771,7 +649,11 @@ function beforeAll(fn) {
771
649
  fn
772
650
  });
773
651
  }
774
- /** Run once after the entire test run (reverse registration order). */
652
+ /**
653
+ * Run once after the entire run, once every scenario is done. Multiple
654
+ * `afterAll` hooks run **in parallel** with no ordering between them — sequence
655
+ * dependent teardown inside a single hook.
656
+ */
775
657
  function afterAll(fn) {
776
658
  globalHookRegistry.add({
777
659
  scope: "global",