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;
}
|