All files / src targeting.ts

80% Statements 44/55
80.85% Branches 38/47
83.33% Functions 10/12
86.66% Lines 39/45

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107            6x 21x 15x             6x 6x 6x 6x         35x 35x     35x     68x     96x         106x   106x 106x 44x 12x     12x       12x     9x         9x 9x   9x           9x 1x         8x 8x 9x 2x           6x 7x             2x       2x 2x   2x 6x 10x 4x       2x    
import type { ParsedFeature, Target } from "@featurevisor/types";
 
import type { Datasource } from "./datasource";
 
export type ResolvedTarget = Target & { key: string };
 
export function normalizeOptionValues(value: string | string[] | undefined): string[] {
  if (typeof value === "undefined") return [];
  return Array.isArray(value) ? value : [value];
}
 
export function matchesFeaturePatterns(
  featureKey: string,
  patterns: Target["includeFeatures"],
): boolean {
  Iif (!patterns) return false;
  return normalizeOptionValues(patterns).some((pattern) => {
    const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
    return new RegExp(`^${escaped}$`).test(featureKey);
  });
}
 
export function matchesTargetTags(candidateTags: string[], selector: Target["tags"]): boolean {
  Iif (!selector) return true;
  Iif (Array.isArray(selector)) {
    return selector.some((tag) => candidateTags.includes(tag));
  }
  Iif ("or" in selector) {
    return selector.or.some((tag) => candidateTags.includes(tag));
  }
  return selector.and.every((tag) => candidateTags.includes(tag));
}
 
export function targetIncludesFeature(
  target: Target,
  featureKey: string,
  feature: ParsedFeature,
): boolean {
  Iif (feature.archived === true) return false;
 
  const featureTags = feature.tags || [];
  if (target.tag && !featureTags.includes(target.tag)) return false;
  if (target.tags && !matchesTargetTags(featureTags, target.tags)) return false;
  Iif (target.includeFeatures && !matchesFeaturePatterns(featureKey, target.includeFeatures)) {
    return false;
  }
  Iif (target.excludeFeatures && matchesFeaturePatterns(featureKey, target.excludeFeatures)) {
    return false;
  }
 
  return true;
}
 
export async function resolveTargets(
  datasource: Datasource,
  requestedTargets?: string | string[],
  options: { defaultToAll?: boolean; requireTargets?: boolean } = {},
): Promise<ResolvedTarget[]> {
  const targetKeys = await datasource.listTargets();
  const requestedTargetKeys = normalizeOptionValues(requestedTargets);
  const selectedKeys =
    requestedTargetKeys.length > 0
      ? requestedTargetKeys
      : options.defaultToAll === false
        ? []
        : targetKeys;
 
  if (options.requireTargets !== false && targetKeys.length === 0) {
    throw new Error(
      'No targets found. Add at least one target definition, for example "targets/all.yml".',
    );
  }
 
  const uniqueKeys = Array.from(new Set(selectedKeys));
  for (const targetKey of uniqueKeys) {
    if (!targetKeys.includes(targetKey)) {
      throw new Error(
        `Unknown target "${targetKey}". Available targets: ${targetKeys.join(", ") || "none"}.`,
      );
    }
  }
 
  return Promise.all(
    uniqueKeys.map(async (targetKey) => ({
      ...(await datasource.readTarget(targetKey)),
      key: targetKey,
    })),
  );
}
 
export async function getTargetFeatureKeys(
  datasource: Datasource,
  targets: ResolvedTarget[],
): Promise<Set<string>> {
  const result = new Set<string>();
  const featureKeys = await datasource.listFeatures();
 
  for (const featureKey of featureKeys) {
    const feature = await datasource.readFeature(featureKey);
    if (targets.some((target) => targetIncludesFeature(target, featureKey, feature))) {
      result.add(featureKey);
    }
  }
 
  return result;
}