flag-prune

Flag rules

A flag rule identifies an exact source shape and assigns its final primitive value.

selector[=value]

The value defaults to true when omitted.

Rule forms

Source shape CLI rule Library definition
Identifier FLAG=false { identifier: "FLAG", value: false }
Member access features.newUi=false { identifier: "features", path: ["newUi"], value: false }
Imported value ./flags#NEW_UI=false { module: "./flags", export: "NEW_UI", value: false }
Call useFlag("new-ui")=false { call: "useFlag", arguments: ["new-ui"], value: false }
Dotted call client.isEnabled("new-ui")=false { call: "client.isEnabled", arguments: ["new-ui"], value: false }
Imported call flag-client#useFlag("new-ui")=false { module: "flag-client", call: "useFlag", arguments: ["new-ui"], value: false }

Quote call rules in your shell so parentheses and spaces are not interpreted.

Identifiers and members

Match a program-level or unresolved identifier:

npx flag-prune --set 'NEW_CHECKOUT=false' src

Match a static member path:

npx flag-prune --set 'features.checkout.newUi=false' src

This matches static dot access and equivalent optional access:

features.checkout.newUi
features?.checkout?.newUi

Computed dynamic keys do not match:

features.checkout[key]

A computed string literal is still static and can match:

features["checkout"].newUi

Imported values

Use the exact module specifier, #, and the imported export name:

npx flag-prune --set './flags#NEW_CHECKOUT=false' src

Given:

import { NEW_CHECKOUT as checkoutEnabled } from "./flags"

The rule matches checkoutEnabled because import aliases are resolved. A local parameter or variable that shadows the alias is not changed.

Namespace imports are supported:

import * as flags from "./flags"

if (flags.NEW_CHECKOUT) {
  // ...
}

The same ./flags#NEW_CHECKOUT=false rule matches the namespace member.

Default imports use default as the export name in the library definition:

{
  module: "flag-client",
  export: "default",
  path: ["newCheckout"],
  value: false,
}

Calls

For SDK examples, see the Unleash, LaunchDarkly, PostHog, Statsig, and OpenFeature guides.

Calls match an exact static callee and an exact required argument prefix:

npx flag-prune --set 'client.isEnabled("new-ui")=false' src

This matches:

client.isEnabled("new-ui")
client.isEnabled("new-ui", context)
client.isEnabled("new-ui", loadContext())

It does not match:

client.isEnabled()
client.isEnabled("other")
client.isEnabled(flagName)

Configured arguments can be strings, numbers, booleans, negative numbers, or null:

npx flag-prune --set 'resolveExperiment("checkout", 2, true, null)=treatment' src

Dynamic configured arguments are rejected because they would make matching ambiguous:

# Invalid
npx flag-prune --set 'useFlag(flagName)=false' src

Additional caller arguments

Arguments after the configured prefix do not participate in matching. Their required evaluation is preserved when the flag call is removed.

if (client.isEnabled("new-ui", loadContext())) {
  renderNewUi()
}

With the value true, the result retains the context call:

loadContext()
renderNewUi()

Pure trailing values can disappear. Calls, getters, computed keys, spreads, and other observable evaluation are retained.

Imported calls

Limit a call rule to one imported binding by adding the module specifier:

npx flag-prune --set 'flag-client#useFlag("new-ui")=false' src

Aliases are resolved, and shadowed functions are not matched.

Without module, a call rule binds to a program-level import or declaration when one exists. Otherwise, an unresolved function name may match. Dotted calls such as client.isEnabled can also match a local parameter or object binding with that exact static path.

Replacement values

Supported values are:

Kind Examples
Boolean true, false
Number 0, 25, -1, 3.5
Null null
String treatment, 'pro tier', "pro tier"
Array [10, 20], ["a", "b"]
Object { enabled: true, name: "treatment" }

Unquoted tokens that are not booleans, numbers, or null are strings:

npx flag-prune --set 'getVariant("checkout")=treatment' src

Quote a string value when it contains spaces or shell-sensitive characters:

npx flag-prune --set 'getVariant("checkout")="new treatment"' src

Object and array variants

A value that begins with { or [ is parsed as a static object or array literal. Use this for SDKs that return a variant object such as Unleash getVariant or OpenFeature useFlag:

npx flag-prune --set 'getVariant("checkout")={ enabled: true, name: "treatment" }' src

Object and array values may nest, and their leaves must be static booleans, numbers, strings, or null. Keys may be bare identifiers or quoted strings:

npx flag-prune --set 'useFlag("theme")={ payload: { mode: "dark" }, "content-type": "json" }' src

Quote the whole rule in your shell so the braces and spaces are passed through intact.

When a flag resolves to an object or array, flag-prune folds static member and index reads to their configured values:

const variant = getVariant("checkout")
if (variant.enabled && variant.name === "treatment") {
  showTreatment()
}

With getVariant("checkout")={ enabled: true, name: "treatment" }, this becomes:

showTreatment()

Object identity is preserved. The declaration is kept when the whole value is still used elsewhere (for example passed to a function), and only the member reads are folded. A declaration whose reads are all folded is removed as unused.

Matching is exact and scope-aware

flag-prune does not use fuzzy text replacement.

These constraints are what make the transform repeatable and conservative.

Explicit local-binding matching

A member selector normally ignores function-local variables and parameters. Use --allow-local-bindings only when matching stable application-owned bindings by name is intentional:

npx flag-prune \
  --set 'hasFeature.newAccessControl=true' \
  --allow-local-bindings \
  --write src
const hasFeature = useFeatures()
if (hasFeature.newAccessControl) useNewAccess()

The member read folds, while the potentially effectful useFeatures() call is retained. Reassigned bindings are still rejected. Because replacing a member also removes any getter or proxy-trap evaluation on that member, this option is appropriate only when that property read is known to be safe to replace.

Multiple rules

Repeat --set:

npx flag-prune \
  --set 'A=true' \
  --set 'B=false' \
  --set 'getVariant("checkout")=control' \
  src

Use -- before a target whose name begins with -:

npx flag-prune --set 'FLAG=false' -- --generated.ts