@autometa/cli 1.0.0-rc.4 → 1.0.0-rc.6

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/dist/bin.cjs CHANGED
@@ -2010,19 +2010,112 @@ function detectRunner(config, cwd) {
2010
2010
  }
2011
2011
  return "default";
2012
2012
  }
2013
+ function normalizeGlobLikePath(input) {
2014
+ return input.replace(/\\/g, "/").replace(/^\.\//u, "");
2015
+ }
2016
+ function patternIsUnderRoot(pattern, root) {
2017
+ const p = normalizeGlobLikePath(pattern);
2018
+ const r = normalizeGlobLikePath(root).replace(/\/+$/u, "");
2019
+ if (!r) {
2020
+ return false;
2021
+ }
2022
+ return p === r || p.startsWith(`${r}/`);
2023
+ }
2024
+ function filterPatternsByGroupsAndModules(options) {
2025
+ const { patterns, groups, modules, config, verbose } = options;
2026
+ const hasGroupSelection = (groups?.length ?? 0) > 0;
2027
+ const hasModuleSelection = (modules?.length ?? 0) > 0;
2028
+ const hasAnySelection = hasGroupSelection || hasModuleSelection;
2029
+ const hasExplicitPatterns = patterns.length > 0;
2030
+ if (hasExplicitPatterns) {
2031
+ if (hasAnySelection && verbose) {
2032
+ const shouldWarn = (() => {
2033
+ const configGroups = config.modules?.groups;
2034
+ if (!configGroups) {
2035
+ return true;
2036
+ }
2037
+ if (hasGroupSelection) {
2038
+ const allowedGroups = new Set(groups);
2039
+ const allowedRoots = Object.entries(configGroups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2040
+ if (allowedRoots.length > 0) {
2041
+ return patterns.some(
2042
+ (pattern) => !allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2043
+ );
2044
+ }
2045
+ }
2046
+ return true;
2047
+ })();
2048
+ if (shouldWarn) {
2049
+ console.warn(
2050
+ "[autometa] Note: when you pass explicit feature patterns, they are used as-is. Group/module filters (-g/-m) affect module/step loading, but do not automatically filter your feature patterns."
2051
+ );
2052
+ }
2053
+ }
2054
+ return patterns;
2055
+ }
2056
+ const roots = [...config.roots.features];
2057
+ if (hasAnySelection && config.modules?.groups) {
2058
+ const allowedGroups = hasGroupSelection ? new Set(groups) : new Set(Object.keys(config.modules.groups));
2059
+ if (hasModuleSelection && modules && modules.length > 0) {
2060
+ const modulePatterns = [];
2061
+ const relativeRoots = config.modules.relativeRoots?.features ?? [".features/**/*.feature"];
2062
+ for (const [groupName, groupConfig] of Object.entries(config.modules.groups)) {
2063
+ if (!allowedGroups.has(groupName)) {
2064
+ continue;
2065
+ }
2066
+ const groupRoot = groupConfig.root;
2067
+ const groupModules = groupConfig.modules ?? [];
2068
+ for (const moduleName of modules) {
2069
+ const moduleExists = Array.isArray(groupModules) && groupModules.some((m) => {
2070
+ if (typeof m === "string") {
2071
+ return m === moduleName || m.endsWith(`/${moduleName}`) || m.endsWith(`:${moduleName}`);
2072
+ }
2073
+ return false;
2074
+ });
2075
+ if (moduleExists) {
2076
+ for (const relativeRoot of relativeRoots) {
2077
+ const normalizedRelative = relativeRoot.replace(/^\.\//, "");
2078
+ const modulePath = path2.join(groupRoot, moduleName, normalizedRelative);
2079
+ modulePatterns.push(modulePath);
2080
+ }
2081
+ }
2082
+ }
2083
+ }
2084
+ if (modulePatterns.length > 0) {
2085
+ return modulePatterns;
2086
+ }
2087
+ }
2088
+ const allowedRoots = Object.entries(config.modules.groups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2089
+ const filtered = roots.filter(
2090
+ (pattern) => allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2091
+ );
2092
+ if (filtered.length > 0) {
2093
+ return filtered;
2094
+ }
2095
+ }
2096
+ return roots;
2097
+ }
2013
2098
  async function orchestrate(options) {
2014
- const { cwd, config, patterns = [], runnerArgs = [], dryRun = false, watch = false, verbose = false } = options;
2099
+ const { cwd, config, patterns = [], runnerArgs = [], dryRun = false, watch = false, verbose = false, groups, modules } = options;
2015
2100
  const runner = detectRunner(config, cwd);
2016
2101
  if (verbose) {
2017
2102
  console.log(`[autometa] Detected runner: ${runner}`);
2018
2103
  }
2104
+ const filteredPatterns = filterPatternsByGroupsAndModules({
2105
+ patterns,
2106
+ groups,
2107
+ modules,
2108
+ config,
2109
+ cwd,
2110
+ verbose
2111
+ });
2019
2112
  switch (runner) {
2020
2113
  case "vitest":
2021
- return spawnVitest({ cwd, patterns, runnerArgs, dryRun, watch, verbose });
2114
+ return spawnVitest({ cwd, patterns: filteredPatterns, runnerArgs, dryRun, watch, verbose });
2022
2115
  case "jest":
2023
- return spawnJest({ cwd, patterns, runnerArgs, dryRun, watch, verbose });
2116
+ return spawnJest({ cwd, patterns: filteredPatterns, runnerArgs, dryRun, watch, verbose });
2024
2117
  case "playwright":
2025
- return spawnPlaywright({ cwd, patterns, runnerArgs, dryRun, watch, verbose });
2118
+ return spawnPlaywright({ cwd, patterns: filteredPatterns, runnerArgs, dryRun, watch, verbose });
2026
2119
  case "default":
2027
2120
  return { success: true, exitCode: 0, runner: "default" };
2028
2121
  }
@@ -2034,7 +2127,8 @@ async function spawnVitest(options) {
2034
2127
  args.push("run");
2035
2128
  }
2036
2129
  if (patterns.length > 0) {
2037
- args.push(...patterns);
2130
+ const expanded = await expandFilePatterns(patterns, cwd);
2131
+ args.push(...expanded.length > 0 ? expanded : patterns);
2038
2132
  }
2039
2133
  if (dryRun) {
2040
2134
  args.push("--passWithNoTests");
@@ -2136,17 +2230,53 @@ var STEP_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"
2136
2230
  var STEP_FALLBACK_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}";
2137
2231
  var FEATURE_FALLBACK_GLOB = "**/*.feature";
2138
2232
  var ROOT_LOAD_ORDER = ["parameterTypes", "support", "hooks", "app"];
2139
- function normalizeGlobLikePath(input) {
2233
+ var GLOB_MAGIC_CHARACTERS = /* @__PURE__ */ new Set(["*", "?", "[", "]", "{", "}", "(", ")", "!"]);
2234
+ function normalizeGlobLikePath2(input) {
2140
2235
  return input.replace(/\\/g, "/").replace(/^\.\//u, "");
2141
2236
  }
2142
- function patternIsUnderRoot(pattern, root) {
2143
- const p = normalizeGlobLikePath(pattern);
2144
- const r = normalizeGlobLikePath(root).replace(/\/+$/u, "");
2237
+ function patternIsUnderRoot2(pattern, root) {
2238
+ const p = normalizeGlobLikePath2(pattern);
2239
+ const r = normalizeGlobLikePath2(root).replace(/\/+$/u, "");
2145
2240
  if (!r) {
2146
2241
  return false;
2147
2242
  }
2148
2243
  return p === r || p.startsWith(`${r}/`);
2149
2244
  }
2245
+ function inferGlobBaseDirectory(pattern) {
2246
+ const normalized = normalizeGlobLikePath2(pattern).replace(/^!/u, "");
2247
+ for (let i = 0; i < normalized.length; i += 1) {
2248
+ if (!GLOB_MAGIC_CHARACTERS.has(normalized[i] ?? "")) {
2249
+ continue;
2250
+ }
2251
+ const prefix = normalized.slice(0, i);
2252
+ const lastSlash = prefix.lastIndexOf("/");
2253
+ if (lastSlash <= 0) {
2254
+ return ".";
2255
+ }
2256
+ const base = prefix.slice(0, lastSlash);
2257
+ return base || ".";
2258
+ }
2259
+ if (path2.extname(normalized)) {
2260
+ return path2.dirname(normalized);
2261
+ }
2262
+ return normalized || ".";
2263
+ }
2264
+ function inferFeatureRootDirectories(patterns, cwd) {
2265
+ if (!patterns || patterns.length === 0) {
2266
+ return [];
2267
+ }
2268
+ const roots = /* @__PURE__ */ new Set();
2269
+ for (const entry of patterns) {
2270
+ const trimmed = entry.trim();
2271
+ if (!trimmed) {
2272
+ continue;
2273
+ }
2274
+ const base = hasGlobMagic(trimmed) ? inferGlobBaseDirectory(trimmed) : trimmed;
2275
+ const resolved = path2.resolve(cwd, base);
2276
+ roots.add(resolved.replace(/\/+$/u, ""));
2277
+ }
2278
+ return Array.from(roots).sort((a, b) => b.length - a.length);
2279
+ }
2150
2280
  function flattenModuleDeclarations(declarations, prefix = []) {
2151
2281
  if (!declarations || declarations.length === 0) {
2152
2282
  return [];
@@ -2230,6 +2360,48 @@ function resolveFileScope(fileAbs, groupIndex) {
2230
2360
  }
2231
2361
  return { kind: "root" };
2232
2362
  }
2363
+ function resolveHoistedDirectoryScope(options) {
2364
+ const rootAbs = options.hoistedFeatureRootsAbs.find(
2365
+ (root) => isPathUnderRoot(options.featureAbsPath, root)
2366
+ );
2367
+ if (!rootAbs) {
2368
+ return { kind: "root" };
2369
+ }
2370
+ const rel = path2.relative(rootAbs, options.featureAbsPath);
2371
+ const segments = normalizePathSegments(rel);
2372
+ const dirSegments = segments.slice(0, -1);
2373
+ if (dirSegments.length === 0) {
2374
+ return { kind: "root" };
2375
+ }
2376
+ const group = dirSegments[0];
2377
+ if (!group) {
2378
+ return { kind: "root" };
2379
+ }
2380
+ const groupEntry = options.groupIndex.find((entry) => entry.group === group);
2381
+ if (!groupEntry) {
2382
+ if (options.strict) {
2383
+ throw new Error(
2384
+ `[autometa] Feature "${options.featureAbsPath}" is under "${rootAbs}" and implies group "${group}", but "${group}" is not declared in config.modules.groups.`
2385
+ );
2386
+ }
2387
+ return { kind: "root" };
2388
+ }
2389
+ const moduleSegments = dirSegments.slice(1);
2390
+ if (moduleSegments.length === 0) {
2391
+ return { kind: "group", group };
2392
+ }
2393
+ for (const modulePath of groupEntry.modulePaths) {
2394
+ if (startsWithSegments(moduleSegments, modulePath)) {
2395
+ return { kind: "module", group, modulePath };
2396
+ }
2397
+ }
2398
+ if (options.strict) {
2399
+ throw new Error(
2400
+ `[autometa] Feature "${options.featureAbsPath}" is under "${rootAbs}" and implies module "${group}:${moduleSegments.join(":")}", but no matching module is declared in config.modules.groups.${group}.modules.`
2401
+ );
2402
+ }
2403
+ return { kind: "group", group };
2404
+ }
2233
2405
  function parseScopeOverrideTag(tags) {
2234
2406
  if (!tags || tags.length === 0) {
2235
2407
  return void 0;
@@ -2253,15 +2425,27 @@ function parseScopeOverrideTag(tags) {
2253
2425
  }
2254
2426
  return void 0;
2255
2427
  }
2256
- function resolveFeatureScope(featureAbsPath, feature, groupIndex) {
2257
- const override = parseScopeOverrideTag(feature.tags);
2428
+ function resolveFeatureScope(options) {
2429
+ const override = parseScopeOverrideTag(options.feature.tags);
2258
2430
  if (override) {
2259
2431
  if (override.modulePath && override.modulePath.length > 0) {
2260
2432
  return { kind: "module", group: override.group, modulePath: override.modulePath };
2261
2433
  }
2262
2434
  return { kind: "group", group: override.group };
2263
2435
  }
2264
- return resolveFileScope(featureAbsPath, groupIndex);
2436
+ const fileScope = resolveFileScope(options.featureAbsPath, options.groupIndex);
2437
+ if (fileScope.kind !== "root") {
2438
+ return fileScope;
2439
+ }
2440
+ if (options.hoistedFeatureScopingMode !== "directory") {
2441
+ return fileScope;
2442
+ }
2443
+ return resolveHoistedDirectoryScope({
2444
+ featureAbsPath: options.featureAbsPath,
2445
+ hoistedFeatureRootsAbs: options.hoistedFeatureRootsAbs,
2446
+ groupIndex: options.groupIndex,
2447
+ strict: options.hoistedFeatureScopingStrict
2448
+ });
2265
2449
  }
2266
2450
  function isVisibleStepScope(stepScope, featureScope) {
2267
2451
  if (featureScope.kind === "root") {
@@ -2354,7 +2538,14 @@ function createFeatureScopePlan(feature, basePlan, options) {
2354
2538
  const scopingMode = options.config.modules?.stepScoping ?? "global";
2355
2539
  const useScopedSteps = scopingMode === "scoped" && options.groupIndex.length > 0;
2356
2540
  const allSteps = Array.from(basePlan.stepsById.values());
2357
- const featureFileScope = useScopedSteps ? resolveFeatureScope(options.featureAbsPath, feature, options.groupIndex) : { kind: "root" };
2541
+ const featureFileScope = useScopedSteps ? resolveFeatureScope({
2542
+ featureAbsPath: options.featureAbsPath,
2543
+ feature,
2544
+ groupIndex: options.groupIndex,
2545
+ hoistedFeatureRootsAbs: options.hoistedFeatureRootsAbs,
2546
+ hoistedFeatureScopingMode: options.config.modules?.hoistedFeatures?.scope ?? "tag",
2547
+ hoistedFeatureScopingStrict: options.config.modules?.hoistedFeatures?.strict ?? true
2548
+ }) : { kind: "root" };
2358
2549
  const visibleSteps = useScopedSteps ? allSteps.filter((definition) => {
2359
2550
  const file = definition.source?.file;
2360
2551
  const stepScope = file ? resolveFileScope(path2.resolve(options.cwd, file), options.groupIndex) : { kind: "root" };
@@ -2481,7 +2672,9 @@ async function runFeatures(options = {}) {
2481
2672
  ...options.runnerArgs ? { runnerArgs: options.runnerArgs } : {},
2482
2673
  ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {},
2483
2674
  ...options.watch !== void 0 ? { watch: options.watch } : {},
2484
- ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
2675
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {},
2676
+ groups: options.groups,
2677
+ modules: options.modules
2485
2678
  });
2486
2679
  if (orchestratorResult.runner !== "default") {
2487
2680
  return {
@@ -2523,7 +2716,7 @@ async function runFeatures(options = {}) {
2523
2716
  const allowedRoots = Object.entries(groups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2524
2717
  if (allowedRoots.length > 0) {
2525
2718
  return patterns.some(
2526
- (pattern) => !allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2719
+ (pattern) => !allowedRoots.some((root) => patternIsUnderRoot2(pattern, root))
2527
2720
  );
2528
2721
  }
2529
2722
  }
@@ -2544,7 +2737,7 @@ async function runFeatures(options = {}) {
2544
2737
  const allowedGroups = (options.groups?.length ?? 0) > 0 ? new Set(options.groups) : new Set(Object.keys(executorConfig.modules.groups));
2545
2738
  const allowedRoots = Object.entries(executorConfig.modules.groups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2546
2739
  const filtered = roots.filter(
2547
- (pattern) => allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2740
+ (pattern) => allowedRoots.some((root) => patternIsUnderRoot2(pattern, root))
2548
2741
  );
2549
2742
  if (filtered.length > 0) {
2550
2743
  return filtered;
@@ -2575,6 +2768,7 @@ async function runFeatures(options = {}) {
2575
2768
  }
2576
2769
  const groupIndex = buildGroupIndex(executorConfig, cwd);
2577
2770
  const environmentIndex = indexStepsEnvironments(stepsEnvironments, cwd, groupIndex);
2771
+ const hoistedFeatureRootsAbs = inferFeatureRootDirectories(executorConfig.roots.features, cwd);
2578
2772
  for (const featurePath of featureFiles) {
2579
2773
  const feature = await readFeatureFile(featurePath, cwd);
2580
2774
  const featureAbsPath = path2.resolve(cwd, featurePath);
@@ -2582,7 +2776,9 @@ async function runFeatures(options = {}) {
2582
2776
  featureAbsPath,
2583
2777
  feature,
2584
2778
  environmentIndex,
2585
- groupIndex
2779
+ groupIndex,
2780
+ executorConfig,
2781
+ hoistedFeatureRootsAbs
2586
2782
  );
2587
2783
  runner.CucumberRunner.setSteps(selectedStepsEnvironment);
2588
2784
  const basePlan = selectedStepsEnvironment.getPlan();
@@ -2590,7 +2786,8 @@ async function runFeatures(options = {}) {
2590
2786
  featureAbsPath,
2591
2787
  cwd,
2592
2788
  config: executorConfig,
2593
- groupIndex
2789
+ groupIndex,
2790
+ hoistedFeatureRootsAbs
2594
2791
  });
2595
2792
  const coordinated = selectedStepsEnvironment.coordinateFeature({
2596
2793
  feature,
@@ -2697,8 +2894,15 @@ function indexStepsEnvironments(environments, cwd, groupIndex) {
2697
2894
  }
2698
2895
  return entries;
2699
2896
  }
2700
- function resolveFeatureStepsEnvironment(featureAbsPath, feature, environments, groupIndex) {
2701
- const featureScope = resolveFeatureScope(featureAbsPath, feature, groupIndex);
2897
+ function resolveFeatureStepsEnvironment(featureAbsPath, feature, environments, groupIndex, config, hoistedFeatureRootsAbs) {
2898
+ const featureScope = resolveFeatureScope({
2899
+ featureAbsPath,
2900
+ feature,
2901
+ groupIndex,
2902
+ hoistedFeatureRootsAbs,
2903
+ hoistedFeatureScopingMode: config.modules?.hoistedFeatures?.scope ?? "tag",
2904
+ hoistedFeatureScopingStrict: config.modules?.hoistedFeatures?.strict ?? true
2905
+ });
2702
2906
  if (featureScope.kind === "root") {
2703
2907
  const root = environments.find((entry) => entry.kind === "root");
2704
2908
  return root?.environment ?? environments[0]?.environment ?? runner.CucumberRunner.steps();