@autometa/cli 1.0.0-rc.5 → 1.0.0-rc.7

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.js CHANGED
@@ -2006,19 +2006,112 @@ function detectRunner(config, cwd) {
2006
2006
  }
2007
2007
  return "default";
2008
2008
  }
2009
+ function normalizeGlobLikePath(input) {
2010
+ return input.replace(/\\/g, "/").replace(/^\.\//u, "");
2011
+ }
2012
+ function patternIsUnderRoot(pattern, root) {
2013
+ const p = normalizeGlobLikePath(pattern);
2014
+ const r = normalizeGlobLikePath(root).replace(/\/+$/u, "");
2015
+ if (!r) {
2016
+ return false;
2017
+ }
2018
+ return p === r || p.startsWith(`${r}/`);
2019
+ }
2020
+ function filterPatternsByGroupsAndModules(options) {
2021
+ const { patterns, groups, modules, config, verbose } = options;
2022
+ const hasGroupSelection = (groups?.length ?? 0) > 0;
2023
+ const hasModuleSelection = (modules?.length ?? 0) > 0;
2024
+ const hasAnySelection = hasGroupSelection || hasModuleSelection;
2025
+ const hasExplicitPatterns = patterns.length > 0;
2026
+ if (hasExplicitPatterns) {
2027
+ if (hasAnySelection && verbose) {
2028
+ const shouldWarn = (() => {
2029
+ const configGroups = config.modules?.groups;
2030
+ if (!configGroups) {
2031
+ return true;
2032
+ }
2033
+ if (hasGroupSelection) {
2034
+ const allowedGroups = new Set(groups);
2035
+ const allowedRoots = Object.entries(configGroups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2036
+ if (allowedRoots.length > 0) {
2037
+ return patterns.some(
2038
+ (pattern) => !allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2039
+ );
2040
+ }
2041
+ }
2042
+ return true;
2043
+ })();
2044
+ if (shouldWarn) {
2045
+ console.warn(
2046
+ "[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."
2047
+ );
2048
+ }
2049
+ }
2050
+ return patterns;
2051
+ }
2052
+ const roots = [...config.roots.features];
2053
+ if (hasAnySelection && config.modules?.groups) {
2054
+ const allowedGroups = hasGroupSelection ? new Set(groups) : new Set(Object.keys(config.modules.groups));
2055
+ if (hasModuleSelection && modules && modules.length > 0) {
2056
+ const modulePatterns = [];
2057
+ const relativeRoots = config.modules.relativeRoots?.features ?? [".features/**/*.feature"];
2058
+ for (const [groupName, groupConfig] of Object.entries(config.modules.groups)) {
2059
+ if (!allowedGroups.has(groupName)) {
2060
+ continue;
2061
+ }
2062
+ const groupRoot = groupConfig.root;
2063
+ const groupModules = groupConfig.modules ?? [];
2064
+ for (const moduleName of modules) {
2065
+ const moduleExists = Array.isArray(groupModules) && groupModules.some((m) => {
2066
+ if (typeof m === "string") {
2067
+ return m === moduleName || m.endsWith(`/${moduleName}`) || m.endsWith(`:${moduleName}`);
2068
+ }
2069
+ return false;
2070
+ });
2071
+ if (moduleExists) {
2072
+ for (const relativeRoot of relativeRoots) {
2073
+ const normalizedRelative = relativeRoot.replace(/^\.\//, "");
2074
+ const modulePath = join(groupRoot, moduleName, normalizedRelative);
2075
+ modulePatterns.push(modulePath);
2076
+ }
2077
+ }
2078
+ }
2079
+ }
2080
+ if (modulePatterns.length > 0) {
2081
+ return modulePatterns;
2082
+ }
2083
+ }
2084
+ const allowedRoots = Object.entries(config.modules.groups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2085
+ const filtered = roots.filter(
2086
+ (pattern) => allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2087
+ );
2088
+ if (filtered.length > 0) {
2089
+ return filtered;
2090
+ }
2091
+ }
2092
+ return roots;
2093
+ }
2009
2094
  async function orchestrate(options) {
2010
- const { cwd, config, patterns = [], runnerArgs = [], dryRun = false, watch = false, verbose = false } = options;
2095
+ const { cwd, config, patterns = [], runnerArgs = [], dryRun = false, watch = false, verbose = false, groups, modules } = options;
2011
2096
  const runner = detectRunner(config, cwd);
2012
2097
  if (verbose) {
2013
2098
  console.log(`[autometa] Detected runner: ${runner}`);
2014
2099
  }
2100
+ const filteredPatterns = filterPatternsByGroupsAndModules({
2101
+ patterns,
2102
+ groups,
2103
+ modules,
2104
+ config,
2105
+ cwd,
2106
+ verbose
2107
+ });
2015
2108
  switch (runner) {
2016
2109
  case "vitest":
2017
- return spawnVitest({ cwd, patterns, runnerArgs, dryRun, watch, verbose });
2110
+ return spawnVitest({ cwd, patterns: filteredPatterns, runnerArgs, dryRun, watch, verbose });
2018
2111
  case "jest":
2019
- return spawnJest({ cwd, patterns, runnerArgs, dryRun, watch, verbose });
2112
+ return spawnJest({ cwd, patterns: filteredPatterns, runnerArgs, dryRun, watch, verbose });
2020
2113
  case "playwright":
2021
- return spawnPlaywright({ cwd, patterns, runnerArgs, dryRun, watch, verbose });
2114
+ return spawnPlaywright({ cwd, patterns: filteredPatterns, runnerArgs, dryRun, watch, verbose });
2022
2115
  case "default":
2023
2116
  return { success: true, exitCode: 0, runner: "default" };
2024
2117
  }
@@ -2030,7 +2123,8 @@ async function spawnVitest(options) {
2030
2123
  args.push("run");
2031
2124
  }
2032
2125
  if (patterns.length > 0) {
2033
- args.push(...patterns);
2126
+ const expanded = await expandFilePatterns(patterns, cwd);
2127
+ args.push(...expanded.length > 0 ? expanded : patterns);
2034
2128
  }
2035
2129
  if (dryRun) {
2036
2130
  args.push("--passWithNoTests");
@@ -2132,17 +2226,53 @@ var STEP_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"
2132
2226
  var STEP_FALLBACK_GLOB = "**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}";
2133
2227
  var FEATURE_FALLBACK_GLOB = "**/*.feature";
2134
2228
  var ROOT_LOAD_ORDER = ["parameterTypes", "support", "hooks", "app"];
2135
- function normalizeGlobLikePath(input) {
2229
+ var GLOB_MAGIC_CHARACTERS = /* @__PURE__ */ new Set(["*", "?", "[", "]", "{", "}", "(", ")", "!"]);
2230
+ function normalizeGlobLikePath2(input) {
2136
2231
  return input.replace(/\\/g, "/").replace(/^\.\//u, "");
2137
2232
  }
2138
- function patternIsUnderRoot(pattern, root) {
2139
- const p = normalizeGlobLikePath(pattern);
2140
- const r = normalizeGlobLikePath(root).replace(/\/+$/u, "");
2233
+ function patternIsUnderRoot2(pattern, root) {
2234
+ const p = normalizeGlobLikePath2(pattern);
2235
+ const r = normalizeGlobLikePath2(root).replace(/\/+$/u, "");
2141
2236
  if (!r) {
2142
2237
  return false;
2143
2238
  }
2144
2239
  return p === r || p.startsWith(`${r}/`);
2145
2240
  }
2241
+ function inferGlobBaseDirectory(pattern) {
2242
+ const normalized = normalizeGlobLikePath2(pattern).replace(/^!/u, "");
2243
+ for (let i = 0; i < normalized.length; i += 1) {
2244
+ if (!GLOB_MAGIC_CHARACTERS.has(normalized[i] ?? "")) {
2245
+ continue;
2246
+ }
2247
+ const prefix = normalized.slice(0, i);
2248
+ const lastSlash = prefix.lastIndexOf("/");
2249
+ if (lastSlash <= 0) {
2250
+ return ".";
2251
+ }
2252
+ const base = prefix.slice(0, lastSlash);
2253
+ return base || ".";
2254
+ }
2255
+ if (extname(normalized)) {
2256
+ return dirname(normalized);
2257
+ }
2258
+ return normalized || ".";
2259
+ }
2260
+ function inferFeatureRootDirectories(patterns, cwd) {
2261
+ if (!patterns || patterns.length === 0) {
2262
+ return [];
2263
+ }
2264
+ const roots = /* @__PURE__ */ new Set();
2265
+ for (const entry of patterns) {
2266
+ const trimmed = entry.trim();
2267
+ if (!trimmed) {
2268
+ continue;
2269
+ }
2270
+ const base = hasGlobMagic(trimmed) ? inferGlobBaseDirectory(trimmed) : trimmed;
2271
+ const resolved = resolve(cwd, base);
2272
+ roots.add(resolved.replace(/\/+$/u, ""));
2273
+ }
2274
+ return Array.from(roots).sort((a, b) => b.length - a.length);
2275
+ }
2146
2276
  function flattenModuleDeclarations(declarations, prefix = []) {
2147
2277
  if (!declarations || declarations.length === 0) {
2148
2278
  return [];
@@ -2226,6 +2356,48 @@ function resolveFileScope(fileAbs, groupIndex) {
2226
2356
  }
2227
2357
  return { kind: "root" };
2228
2358
  }
2359
+ function resolveHoistedDirectoryScope(options) {
2360
+ const rootAbs = options.hoistedFeatureRootsAbs.find(
2361
+ (root) => isPathUnderRoot(options.featureAbsPath, root)
2362
+ );
2363
+ if (!rootAbs) {
2364
+ return { kind: "root" };
2365
+ }
2366
+ const rel = relative(rootAbs, options.featureAbsPath);
2367
+ const segments = normalizePathSegments(rel);
2368
+ const dirSegments = segments.slice(0, -1);
2369
+ if (dirSegments.length === 0) {
2370
+ return { kind: "root" };
2371
+ }
2372
+ const group = dirSegments[0];
2373
+ if (!group) {
2374
+ return { kind: "root" };
2375
+ }
2376
+ const groupEntry = options.groupIndex.find((entry) => entry.group === group);
2377
+ if (!groupEntry) {
2378
+ if (options.strict) {
2379
+ throw new Error(
2380
+ `[autometa] Feature "${options.featureAbsPath}" is under "${rootAbs}" and implies group "${group}", but "${group}" is not declared in config.modules.groups.`
2381
+ );
2382
+ }
2383
+ return { kind: "root" };
2384
+ }
2385
+ const moduleSegments = dirSegments.slice(1);
2386
+ if (moduleSegments.length === 0) {
2387
+ return { kind: "group", group };
2388
+ }
2389
+ for (const modulePath of groupEntry.modulePaths) {
2390
+ if (startsWithSegments(moduleSegments, modulePath)) {
2391
+ return { kind: "module", group, modulePath };
2392
+ }
2393
+ }
2394
+ if (options.strict) {
2395
+ throw new Error(
2396
+ `[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.`
2397
+ );
2398
+ }
2399
+ return { kind: "group", group };
2400
+ }
2229
2401
  function parseScopeOverrideTag(tags) {
2230
2402
  if (!tags || tags.length === 0) {
2231
2403
  return void 0;
@@ -2249,15 +2421,36 @@ function parseScopeOverrideTag(tags) {
2249
2421
  }
2250
2422
  return void 0;
2251
2423
  }
2252
- function resolveFeatureScope(featureAbsPath, feature, groupIndex) {
2253
- const override = parseScopeOverrideTag(feature.tags);
2424
+ function resolveFeatureScope(options) {
2425
+ const override = parseScopeOverrideTag(options.feature.tags);
2426
+ const fileScope = resolveFileScope(options.featureAbsPath, options.groupIndex);
2427
+ const inferred = (() => {
2428
+ if (fileScope.kind !== "root") {
2429
+ return fileScope;
2430
+ }
2431
+ if (options.hoistedFeatureScopingMode !== "directory") {
2432
+ return fileScope;
2433
+ }
2434
+ return resolveHoistedDirectoryScope({
2435
+ featureAbsPath: options.featureAbsPath,
2436
+ hoistedFeatureRootsAbs: options.hoistedFeatureRootsAbs,
2437
+ groupIndex: options.groupIndex,
2438
+ strict: options.hoistedFeatureScopingStrict
2439
+ });
2440
+ })();
2254
2441
  if (override) {
2255
2442
  if (override.modulePath && override.modulePath.length > 0) {
2256
2443
  return { kind: "module", group: override.group, modulePath: override.modulePath };
2257
2444
  }
2445
+ if (inferred.kind === "module" && inferred.group === override.group) {
2446
+ return inferred;
2447
+ }
2448
+ if (inferred.kind === "group" && inferred.group === override.group) {
2449
+ return inferred;
2450
+ }
2258
2451
  return { kind: "group", group: override.group };
2259
2452
  }
2260
- return resolveFileScope(featureAbsPath, groupIndex);
2453
+ return inferred;
2261
2454
  }
2262
2455
  function isVisibleStepScope(stepScope, featureScope) {
2263
2456
  if (featureScope.kind === "root") {
@@ -2350,7 +2543,14 @@ function createFeatureScopePlan(feature, basePlan, options) {
2350
2543
  const scopingMode = options.config.modules?.stepScoping ?? "global";
2351
2544
  const useScopedSteps = scopingMode === "scoped" && options.groupIndex.length > 0;
2352
2545
  const allSteps = Array.from(basePlan.stepsById.values());
2353
- const featureFileScope = useScopedSteps ? resolveFeatureScope(options.featureAbsPath, feature, options.groupIndex) : { kind: "root" };
2546
+ const featureFileScope = useScopedSteps ? resolveFeatureScope({
2547
+ featureAbsPath: options.featureAbsPath,
2548
+ feature,
2549
+ groupIndex: options.groupIndex,
2550
+ hoistedFeatureRootsAbs: options.hoistedFeatureRootsAbs,
2551
+ hoistedFeatureScopingMode: options.config.modules?.hoistedFeatures?.scope ?? "tag",
2552
+ hoistedFeatureScopingStrict: options.config.modules?.hoistedFeatures?.strict ?? true
2553
+ }) : { kind: "root" };
2354
2554
  const visibleSteps = useScopedSteps ? allSteps.filter((definition) => {
2355
2555
  const file = definition.source?.file;
2356
2556
  const stepScope = file ? resolveFileScope(resolve(options.cwd, file), options.groupIndex) : { kind: "root" };
@@ -2477,7 +2677,9 @@ async function runFeatures(options = {}) {
2477
2677
  ...options.runnerArgs ? { runnerArgs: options.runnerArgs } : {},
2478
2678
  ...options.dryRun !== void 0 ? { dryRun: options.dryRun } : {},
2479
2679
  ...options.watch !== void 0 ? { watch: options.watch } : {},
2480
- ...options.verbose !== void 0 ? { verbose: options.verbose } : {}
2680
+ ...options.verbose !== void 0 ? { verbose: options.verbose } : {},
2681
+ groups: options.groups,
2682
+ modules: options.modules
2481
2683
  });
2482
2684
  if (orchestratorResult.runner !== "default") {
2483
2685
  return {
@@ -2519,7 +2721,7 @@ async function runFeatures(options = {}) {
2519
2721
  const allowedRoots = Object.entries(groups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2520
2722
  if (allowedRoots.length > 0) {
2521
2723
  return patterns.some(
2522
- (pattern) => !allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2724
+ (pattern) => !allowedRoots.some((root) => patternIsUnderRoot2(pattern, root))
2523
2725
  );
2524
2726
  }
2525
2727
  }
@@ -2540,7 +2742,7 @@ async function runFeatures(options = {}) {
2540
2742
  const allowedGroups = (options.groups?.length ?? 0) > 0 ? new Set(options.groups) : new Set(Object.keys(executorConfig.modules.groups));
2541
2743
  const allowedRoots = Object.entries(executorConfig.modules.groups).filter(([group]) => allowedGroups.has(group)).map(([, groupConfig]) => groupConfig.root);
2542
2744
  const filtered = roots.filter(
2543
- (pattern) => allowedRoots.some((root) => patternIsUnderRoot(pattern, root))
2745
+ (pattern) => allowedRoots.some((root) => patternIsUnderRoot2(pattern, root))
2544
2746
  );
2545
2747
  if (filtered.length > 0) {
2546
2748
  return filtered;
@@ -2571,6 +2773,7 @@ async function runFeatures(options = {}) {
2571
2773
  }
2572
2774
  const groupIndex = buildGroupIndex(executorConfig, cwd);
2573
2775
  const environmentIndex = indexStepsEnvironments(stepsEnvironments, cwd, groupIndex);
2776
+ const hoistedFeatureRootsAbs = inferFeatureRootDirectories(executorConfig.roots.features, cwd);
2574
2777
  for (const featurePath of featureFiles) {
2575
2778
  const feature = await readFeatureFile(featurePath, cwd);
2576
2779
  const featureAbsPath = resolve(cwd, featurePath);
@@ -2578,7 +2781,9 @@ async function runFeatures(options = {}) {
2578
2781
  featureAbsPath,
2579
2782
  feature,
2580
2783
  environmentIndex,
2581
- groupIndex
2784
+ groupIndex,
2785
+ executorConfig,
2786
+ hoistedFeatureRootsAbs
2582
2787
  );
2583
2788
  CucumberRunner.setSteps(selectedStepsEnvironment);
2584
2789
  const basePlan = selectedStepsEnvironment.getPlan();
@@ -2586,7 +2791,8 @@ async function runFeatures(options = {}) {
2586
2791
  featureAbsPath,
2587
2792
  cwd,
2588
2793
  config: executorConfig,
2589
- groupIndex
2794
+ groupIndex,
2795
+ hoistedFeatureRootsAbs
2590
2796
  });
2591
2797
  const coordinated = selectedStepsEnvironment.coordinateFeature({
2592
2798
  feature,
@@ -2693,8 +2899,15 @@ function indexStepsEnvironments(environments, cwd, groupIndex) {
2693
2899
  }
2694
2900
  return entries;
2695
2901
  }
2696
- function resolveFeatureStepsEnvironment(featureAbsPath, feature, environments, groupIndex) {
2697
- const featureScope = resolveFeatureScope(featureAbsPath, feature, groupIndex);
2902
+ function resolveFeatureStepsEnvironment(featureAbsPath, feature, environments, groupIndex, config, hoistedFeatureRootsAbs) {
2903
+ const featureScope = resolveFeatureScope({
2904
+ featureAbsPath,
2905
+ feature,
2906
+ groupIndex,
2907
+ hoistedFeatureRootsAbs,
2908
+ hoistedFeatureScopingMode: config.modules?.hoistedFeatures?.scope ?? "tag",
2909
+ hoistedFeatureScopingStrict: config.modules?.hoistedFeatures?.strict ?? true
2910
+ });
2698
2911
  if (featureScope.kind === "root") {
2699
2912
  const root = environments.find((entry) => entry.kind === "root");
2700
2913
  return root?.environment ?? environments[0]?.environment ?? CucumberRunner.steps();