Configuration
@evanion/feature 0.1.0 and its patch releases up to 0.1.1, the version on npm. Values on this page were produced by running the source at e53b2d8, a later commit than the 0.1.0 tag, and are not re-executed.
To move your feature flags out of component logic and into a central store, you use createFeatures. This function takes your feature definitions, validates them, and returns the store every other call reads. A rule in a definition replaces the role === 'bookseller' check each component would otherwise repeat.
Declarative Feature Definitions
@evanion/feature uses a declarative configuration so you can define the intent (is it enabled?) and the rules (when is it active?) in a single place. createFeatures validates the dependency graph, deep-clones and freezes your definitions, and returns the store.
import { } from '@evanion/feature';
const = [{ : 'gift-cards', : true }];
const = ();
.('gift-cards'); // -> true
.([0]); // -> false
.(.('gift-cards')); // -> trueThe store holds a frozen copy of each definition. Your own array stays editable, and an assignment to the copy definition() returns throws a TypeError, so the store never diverges from what it resolves.
The Configuration Schema
| Field | Meaning |
|---|---|
key | The unique identifier for the feature. A string or a number. |
enabled | The kill switch. If false, the feature is off for everyone. |
dependsOn | A list of other features that must resolve on for this one to activate. |
rules | Activation rules (OR-ed). If any rule matches, the feature is active. |
seed | A bucketing seed for rollouts. Defaults to the key. |
freezeTimeAtBuild | If true, plan() resolves time windows at build-time for static sites. |
variants | The named variants this feature splits across (see Variants). |
variantBy | The context field used to bucket variants. Defaults to targetingKey. |
variantSeed | Bucketing seed for the variant split. Defaults to the seed (or key) plus :variant. |
A rule carries an optional id, a when list of conditions, an optional rollout, and an optional variant it pins. A condition names a context field, an op and a value.
Resolution Precedence
When you call resolve(), @evanion/feature follows a strict hierarchy to determine if a feature is active.
- The Kill Switch: If
enabled === false, the feature is off. No rules are evaluated. - The Dependency Chain: If any feature in
dependsOnresolves tofalse, this feature is off. No rules are evaluated. - The Default State: If
enabled === trueand there are no rules, the feature is on. - The Rules: If rules exist, they are OR-ed. If any rule matches, the feature is on. Inside a rule, conditions are AND-ed.
enabled does not depend on rule order. rule names the first rule that matched, and that rule’s variant pin is the one applied, so moving a rule up can change which rule a decision names.
The kill switch works in one direction only. Setting enabled: true on a feature whose rules do not match changes the stored intent, and the feature still resolves off.
Dependency Cascades
@evanion/feature cascades dependencies transitively and one way. With demo night booking behind express pickup, and express pickup behind the new checkout, express pickup being off takes demo night booking off and leaves the new checkout on.
import { } from '@evanion/feature';
const = ([
{ : 'demo-night-booking', : true, : ['express-pickup'] },
{ : 'express-pickup', : false, : ['new-checkout'] },
{ : 'new-checkout', : true },
]);
.('demo-night-booking'); // -> false
.('new-checkout'); // -> truecreateFeatures evaluates in dependency order, so the cascade works at any depth and you can declare your features in any order.
Configuration Errors
createFeatures validates the dependency graph at construction. It throws a subclass of FeatureConfigError if it detects:
- Cycles:
FeatureCycleError, e.g.,Adepends onB, andBdepends onA. Itspathproperty holds the cycle’s keys, from the first key back to itself. - Unknown Dependencies:
UnknownDependencyError, when a feature depends on a key that isn’t configured. - Duplicates:
DuplicateFeatureError, when two features share the same key.
import { } from '@evanion/feature';
import type { } from '@evanion/feature';
const = (: []) => {
try {
();
return 'built';
} catch () {
return ();
}
};
const : [] = [
{ : 'a', : true, : ['c'] },
{ : 'b', : true, : ['a'] },
{ : 'c', : true, : ['b'] },
];
const : [] = [
{ : 'a', : true },
{ : 'a', : true },
];
(); // -> 'FeatureCycleError: feature dependency cycle: a -> c -> b -> a'
([{ : 'a', : true, : ['b'] }]); // -> 'UnknownDependencyError: feature "a" depends on "b", which is not configured'
(); // -> 'DuplicateFeatureError: duplicate feature key "a"'Variants are checked at the same point: DuplicateVariantError for two variants sharing a name, and UnknownVariantError for a rule pinning a variant its feature does not declare. Once createFeatures returns, resolve, plan and toggle never throw.
Defining Rules and Conditions
To activate features based on the user’s context, you use @evanion/feature rules. In the example below, a bookseller matches the first rule, and every other customer is subject to the second rule’s 10% rollout:
import { } from '@evanion/feature';
const = ([
{
: 'beta-banner',
: true,
: [
{
: 'internal',
: [{ : 'roles', : 'contains', : 'bookseller' }],
},
{ : 'ramp', : { : 10 } },
],
},
]);
.('beta-banner', { : ['bookseller'] }); // -> true
const = (: string) =>
.('beta-banner', { : ['customer'], });
('cust-0042'); // -> false
('cust-0007'); // -> trueA rule that declares no id is named by a hash of what it matches on, so its name survives a rule being inserted above it. A decision names the rule in rule, and in its breakdown when no rule matched. The hash leaves out rollout.percent, so two rollout rules on one feature that differ only in their percentage share a name. Give a rule you expect to read in a log an explicit id.
Condition Operators
op | Effect | Example |
|---|---|---|
before / after | Checks if now is strictly before/after an instant. | value: '2026-10-01T00:00:00Z' |
day-of-week | Checks if now falls on specific weekdays in a zone. | zone: 'Europe/Stockholm', value: ['mon'] |
eq / ne | Strict equality or inequality. | value: 'bookseller' |
in / not-in | Checks if the value is in/not in an array. | value: ['bookseller', 'owner'] |
contains | Checks if the field (array) contains the value. | value: 'bookseller' |
A day-of-week condition has no default zone: it names an IANA zone, because UTC day-of-week is wrong for a shop open in Stockholm.
Missing Context Fields
In @evanion/feature, a condition over a field the context does not carry never holds, and that includes ne and not-in. Treating ne on an absent field as true would switch features on for exactly the contexts that carry the least information.
import { } from '@evanion/feature';
const = ([
{
: 'new-checkout',
: true,
: [
{
: 'not-customers',
: [{ : 'role', : 'ne', : 'customer' }],
},
],
},
]);
.('new-checkout', { : 'bookseller' }); // -> true
.('new-checkout', {}); // -> falseTime Windows
To read the same on every host, a date-time string must include an offset (e.g., +09:00, +0900 or Z). A date with no time is read as UTC midnight. createFeatures rejects a date-time without an offset, and a date the calendar does not have, at construction.
import { } from '@evanion/feature';
const = (: string) => {
try {
([
{
: 'new-checkout',
: true,
: [{ : 'launch', : [{ : 'now', : 'after', }] }],
},
]);
return 'built';
} catch () {
return ( as Error).;
}
};
('2026-01-01T09:00:00+09:00'); // -> 'built'
('2026-01-01T09:00:00+0900'); // -> 'built'
('2026-01-01'); // -> 'built'
('2026-01-01T00:00:00'); // -> 'FeatureConfigError'
('2026-04-31'); // -> 'FeatureConfigError'A window’s value can also be epoch milliseconds or a Date. createFeatures refuses NaN and a number outside the range a Date holds, and it takes a Date as the instant it holds, so new Date('2026-01-01T00:00:00') carries the zone of the host that built it.
Type-Safe Keys and Variants
You can maintain type safety for your @evanion/feature configuration depending on how your data is stored. If you define your configuration as an array literal, TypeScript infers the keys and variant names automatically.
If your configuration comes from a JSON file or variable, you can provide a schema interface:
import { } from '@evanion/feature';
import type { } from '@evanion/feature';
interface Flags {
'new-checkout': never;
'express-pickup': { : 'control' | 'blue'; : never };
}
// Typed as definitions, so the compiler has no literals to read.
const : <keyof Flags>[] = [
{ : 'new-checkout', : true },
{
: 'express-pickup',
: true,
: ['new-checkout'],
: [
{ : 'control', : 50 },
{ : 'blue', : 50 },
],
},
];
const = <Flags>();
.('express-pickup', { : 'cust-0042' }); // -> 'blue'
// @ts-expect-error -- 'express-delivery' is not a key of Flags
.('express-delivery'); // -> falseThis ensures that calling features.isEnabled('express-delivery') is a compile error, and features.variantOf('express-pickup') returns 'control' | 'blue' | undefined. createFeatures<Flags>(config) also checks every definition’s key against keyof Flags.
Decisions resolves this store and reads what each decision says about the rule that decided it.