Feature flags for TypeScript with Cargo-style feature definitions, conditional compilation, and CLI tooling.
ft-flags provides a robust feature flag system for TypeScript that follows the same conventions as Cargo features in Rust. Features can be:
- Declared statically in your
deno.jsonorpackage.json - Composed together into feature sets
- Enabled by default or opt-in
- Validated at build time via JSON schema
- Queried via CLI for scripting and debugging
This package serves as the foundation for conditional compilation in the @hiisi/cfg-ts ecosystem.
Multi-platform package: This package is available for both Deno (JSR) and Node.js/Bun (npm). We produce slightly different builds optimized for each platform, with equivalent JSR ↔ npm package mappings where applicable.
The following runtimes and versions are tested in CI. The package may work on other versions, but only these are officially verified:
| 1.x | 2.x |
|---|---|
| âś… |
1.x is best-effort due to lockfile version incompatibility
| 18 | 20 | 22 |
|---|---|---|
| âś… | âś… | âś… |
| 1.0 | latest |
|---|---|
| âś… | âś… |
Bun support is best-effort
# Deno
deno add jsr:@hiisi/ft-flags
# npm / yarn / pnpm
npm install ft-flagsFeatures are declared at the root level of your deno.json or package.json. The format follows Cargo's conventions:
{
"name": "@my/package",
"version": "1.0.0",
"features": {
"default": ["std"],
"std": ["fs", "env"],
"full": ["std", "experimental"],
"fs": [],
"env": [],
"args": [],
"experimental": ["async-runtime"],
"async-runtime": []
}
}Feature names follow Cargo conventions:
- Kebab-case: Feature names use lowercase with hyphens:
async-runtime,serde-support :for dependency features: Enable features from dependencies:lodash:clone,@scope/pkg:featuredep:for optional deps: Enable optional dependencies:dep:tokio
Note: We use
:instead of/(which Cargo uses) to avoid ambiguity with scoped package names like@scope/pkgthat are common in the JS/TS ecosystem.
The default feature is special — it lists the features that are enabled when no explicit feature selection is made. This is equivalent to Cargo's default feature.
{
"features": {
"default": ["std", "logging"]
}
}To disable default features, use the --no-default-features CLI flag or set defaultFeatures: false in your config.
Each feature maps to an array of features it activates. When you enable a feature, all features it lists are also enabled (transitively).
{
"features": {
"full": ["std", "experimental", "async-runtime"],
"std": ["fs", "env"]
}
}Enabling full will enable: full, std, experimental, async-runtime, fs, env.
Enable features from your dependencies using ::
{
"features": {
"serialization": ["serde:derive", "@myorg/utils:json"],
"async": ["tokio:full"]
}
}Similar to Cargo's dep: syntax, you can reference optional package dependencies:
{
"features": {
"async": ["dep:async-hooks"],
"tracing": ["dep:opentelemetry"]
}
}Note: dep: integration with package managers is planned for a future release.
You can add metadata to features for documentation and tooling. Metadata uses the metadata.features namespace, following the convention used by Cargo's [package.metadata.X]:
{
"name": "@my/package",
"features": {
"default": ["std"],
"std": ["fs", "env"],
"experimental": []
},
"metadata": {
"features": {
"std": {
"description": "Standard library features for cross-runtime compatibility"
},
"experimental": {
"description": "Unstable features that may change",
"unstable": true
},
"legacy-api": {
"description": "Use the new API instead",
"deprecated": true,
"deprecatedMessage": "Migrate to v2-api feature"
}
}
}
}{
"name": "@my/package",
"features": {
"default": ["..."],
"feature-name": ["dependency1", "dependency2"]
},
"metadata": {
"features": {
"feature-name": {
"description": "Human-readable description",
"since": "1.0.0",
"unstable": false,
"deprecated": false,
"deprecatedMessage": "..."
}
}
}
}Override features at runtime via environment variables:
# Enable specific features
FT_FEATURES=experimental,async-runtime
# Disable default features
FT_NO_DEFAULT_FEATURES=true
# Enable all features
FT_ALL_FEATURES=truePass feature flags via command line:
my-app --features experimental,async-runtime
my-app --no-default-features
my-app --all-featuresft-flags includes a CLI (ft) for querying and validating features.
# Global install via Deno
deno install -A -n ft jsr:@hiisi/ft-flags/cli
# Or run directly
deno run -A jsr:@hiisi/ft-flags/cli <command>
# Or via deno task (when in a project with ft-flags)
deno task ft <command>List all available features for the current package.
$ ft list
Available features:
default -> [std]
std -> [fs, env]
fs -> []
env -> []
experimental -> []
$ ft list --enabled
Enabled features (with default):
[ok] default
[ok] std
[ok] fs
[ok] envCheck if a specific feature is enabled.
$ ft check fs
[ok] fs is enabled (via: default -> std -> fs)
$ ft check experimental
[x] experimental is not enabled
$ ft check experimental --features experimental
[ok] experimental is enabled (explicit)Exit codes: 0 if enabled, 1 if disabled.
Show the fully resolved set of enabled features.
$ ft resolve
Resolved features:
default, std, fs, env
$ ft resolve --features full --no-default-features
Resolved features:
full, std, experimental, async-runtime, fs, env
$ ft resolve --all-features
Resolved features:
default, std, full, experimental, async-runtime, fs, env, argsDisplay the feature dependency tree.
$ ft tree
Feature tree:
default
`-- std
|-- fs
`-- env
full
|-- std
| |-- fs
| `-- env
`-- experimental
`-- async-runtime
args
$ ft tree full
full
|-- std
| |-- fs
| `-- env
`-- experimental
`-- async-runtimeValidate the feature configuration.
$ ft validate
[ok] Configuration is valid
$ ft validate
[x] Error: Circular dependency detected: full -> experimental -> full
[x] Error: Unknown feature referenced: "nonexistent" in feature "std"Query features for a specific package in a workspace:
$ ft list --package @myorg/subpackage
$ ft check fs --package ./packages/my-libimport {
isFeatureEnabled,
listAvailableFeatures,
loadManifest,
resolveFeatures,
} from "@hiisi/ft-flags";
// Load features from deno.json or package.json
const manifest = await loadManifest();
// Resolve with default features
const resolved = resolveFeatures(manifest);
// Check if a feature is enabled
if (isFeatureEnabled("fs", resolved)) {
// Use filesystem features
}
// List all available features
const available = listAvailableFeatures(manifest);
console.log(available); // ["default", "std", "fs", ...]import { resolveFeatures } from "@hiisi/ft-flags";
// Enable specific features, no defaults
const resolved = resolveFeatures(manifest, {
features: ["experimental", "fs"],
noDefaultFeatures: true,
});
// Enable all features
const all = resolveFeatures(manifest, {
allFeatures: true,
});import { buildSchema, createRegistry, featureId, isEnabled } from "@hiisi/ft-flags";
// Define features with schema
const schema = buildSchema([
{ id: "fs", description: "File system access" },
{ id: "env", description: "Environment variable access" },
{ id: "async-runtime", description: "Async runtime support" },
]);
// Create registry with enabled features
const registry = createRegistry({
schema,
config: {
enabled: ["fs", "env"],
},
});
// Type-safe feature checks
if (isEnabled(registry, featureId("fs"))) {
// ...
}import { validateManifest } from "@hiisi/ft-flags";
const result = validateManifest({
features: {
default: ["std"],
std: ["unknown-feature"], // Error!
},
});
if (!result.valid) {
console.error(result.errors);
// ["Unknown feature 'unknown-feature' referenced in 'std'"]
}A JSON schema is provided for editor validation and autocompletion.
Add to your settings.json:
{
"json.schemas": [
{
"fileMatch": ["deno.json", "package.json"],
"url": "https://jsr.io/@hiisi/ft-flags/schema.json"
}
]
}https://jsr.io/@hiisi/ft-flags/schema.json
ft-flags is designed to work with @hiisi/cfg-ts for conditional compilation:
import { cfg } from "@hiisi/cfg-ts";
// @cfg(feature("fs"))
export function readFile(path: string): string {
// This function is only included when fs is enabled
}
// @cfg(not(feature("experimental")))
export function stableApi(): void {
// Only included when experimental is NOT enabled
}
// @cfg(all(feature("std"), not(feature("legacy"))))
export function modernStdLib(): void {
// Complex predicates
}| Cargo | ft-flags | Notes |
|---|---|---|
[features] |
"features": {} |
Same concept |
default = ["std"] |
"default": ["std"] |
Same semantics |
foo = ["bar", "baz"] |
"foo": ["bar", "baz"] |
Feature enables others |
dep:optional-dep |
"dep:pkg-name" |
Optional dependency |
serde/derive |
"serde:derive" |
Dep feature ref (: instead of /) |
--features foo |
--features foo |
CLI flag |
--no-default-features |
--no-default-features |
Disable defaults |
--all-features |
--all-features |
Enable everything |
@hiisi/cfg-ts- Conditional compilation with@cfg()syntax@hiisi/otso- Build framework that orchestrates feature-based builds@hiisi/tgts- Target definitions (runtime, platform, arch)@hiisi/onlywhen- Runtime feature detection
Whether you use this project, have learned something from it, or just like it, please consider supporting it by buying me a coffee, so I can dedicate more time on open-source projects like this :)
You can check out the full license here
This project is licensed under the terms of the Mozilla Public License 2.0.
SPDX-License-Identifier: MPL-2.0
