ft-flags
v0.1.2
Published
Feature flags for TypeScript with Cargo-style feature definitions, conditional compilation, and CLI tooling
Maintainers
Readme
ft-flags
Feature flags for TypeScript with Cargo-style feature definitions, conditional compilation, and CLI tooling.
Overview
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.
npm package: This is the Node.js and Bun optimized version of ft-flags. For Deno, see @hiisi/ft-flags on JSR.
Supported Runtimes
This is the Node.js/Bun optimized package. The following runtimes and versions are tested in CI:
Node.js
| 18 | 20 | 22 | |:--:|:--:|:--:| | ✅ | ✅ | ✅ |
Bun
| 1.0 | latest | |:---:|:------:| | ⚠️ | ✅ |
Bun support is best-effort
For Deno support, use the JSR package.
Installation
# npm
npm install ft-flags
# yarn
yarn add ft-flags
# pnpm
pnpm add ft-flagsFeature Model
Declaring Features
Features are declared at the root level of your 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": []
}
}Naming Conventions
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.
Key Concepts
The default Feature
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.
Feature Dependencies
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.
Dependency Features
Enable features from your dependencies using ::
{
"features": {
"serialization": ["serde:derive", "@myorg/utils:json"],
"async": ["tokio:full"]
}
}Optional Dependencies
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.
Feature Metadata
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"
}
}
}
}Configuration
Full Configuration Schema
{
"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": "..."
}
}
}
}Environment Variables
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=trueCLI Arguments
Pass feature flags via command line:
my-app --features experimental,async-runtime
my-app --no-default-features
my-app --all-featuresCLI Tool
ft-flags includes a CLI (ft) for querying and validating features.
Installation
# Run via npx
npx ft <command>
# Or install globally
npm install -g ft-flags
ft <command>Commands
ft list
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] envft check <feature>
Check 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.
ft resolve
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, argsft tree [feature]
Display 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-runtimeft validate
Validate 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"Package-Specific Queries
Query features for a specific package in a workspace:
$ ft list --package @myorg/subpackage
$ ft check fs --package ./packages/my-libProgrammatic API
Basic Usage
import {
isFeatureEnabled,
listAvailableFeatures,
loadManifest,
resolveFeatures,
} from "ft-flags";
// Load features from 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", ...]Custom Feature Selection
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,
});Using the Registry API
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"))) {
// ...
}Schema Validation
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'"]
}JSON Schema
A JSON schema is provided for editor validation and autocompletion.
VS Code / Editors
Add to your settings.json:
{
"json.schemas": [
{
"fileMatch": ["deno.json", "package.json"],
"url": "https://jsr.io/@hiisi/ft-flags/schema.json"
}
]
}Schema URL
https://jsr.io/@hiisi/ft-flags/schema.jsonIntegration with cfg-ts
ft-flags is designed to work with cfg-ts for conditional compilation:
import { cfg } from "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
}Comparison with Cargo
| 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 |
Related Packages
cfg-ts- Conditional compilation with@cfg()syntaxotso- Build framework that orchestrates feature-based buildstgts- Target definitions (runtime, platform, arch)onlywhen- Runtime feature detection
Support
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 :)
License
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
