npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jabraf/app-config

v0.2.3

Published

Application configuration and feature flags management for web projects

Readme

@jabraf/app-config

Application configuration and feature flags management for web projects.

Installation

npm install @jabraf/app-config

react is an optional peer dependency. Install it alongside @jabraf/app-config only if you intend to use the useFeature hook.

Overview

@jabraf/app-config is a lightweight configuration and feature-flag layer for Node.js and React applications. It provides:

  • A universal runtime API (setConfig / getConfig) that works identically in browser and Node environments — the consumer statically imports the generated config/app-config.json and hands it to the library at bootstrap.
  • A Node-only buildConfig entrypoint (under the @jabraf/app-config/build subpath) that delegates to a project-owned config/make-config.ts so apps can produce their own configuration artifacts.
  • A React hook (useFeature) plus cookie helpers (setFeatureCookie, removeFeatureCookie) for toggling features at runtime with cookie-based overrides.
  • A jabraf-config CLI with a build subcommand (with an optional --watch mode) for generating config/app-config.json from a project-owned config/make-config.ts.

Entry points

| Import | Environment | Exports | | ----------------------------------------- | --------------- | ------------------------------------------------------- | | @jabraf/app-config | browser + Node | setConfig, getConfig, resetConfig, types | | @jabraf/app-config/hooks/use-feature.js | browser (+ SSR) | useFeature, setFeatureCookie, removeFeatureCookie | | @jabraf/app-config/build | Node only | buildConfig, appDirectory, fromRoot |

The root entry point and the React hooks subpath never import node:* modules and are safe to bundle for the browser.

Project Layout

The package resolves paths relative to the nearest package.json of the consuming project and expects the following files under a config/ directory:

your-app/
└── config/
    ├── make-config.ts           # Authored: build-time script invoked by buildConfig
    └── app-config.json          # Generated: application configuration consumed by getConfig

make-config.ts must export an async buildAppConfig function that receives the resolved environment and returns the AppConfig for that environment (or null if no config applies). buildConfig imports this module, invokes the function, validates the result, and writes config/app-config.json itself — your code does not need to touch the filesystem.

// config/make-config.ts
import type { AppConfig, BuildAppConfig, EnvironmentConfig } from '@jabraf/app-config';

const configByEnv: EnvironmentConfig<AppConfig> = {
  dev: { env: 'dev', hostname: 'localhost:3000', features: { beta: true } },
  test: { env: 'test', hostname: 'test.example.com', features: { beta: true } },
  staging: { env: 'staging', hostname: 'staging.example.com', features: { beta: true } },
  production: { env: 'production', hostname: 'example.com', features: { beta: false } },
};

export const buildAppConfig: BuildAppConfig = async (env) => configByEnv[env] ?? null;

Because config/app-config.json is a build artifact rather than source, add it to your .gitignore:

# config build output from @jabraf/app-config
config/app-config.json

Feature Type

Features are tracked through a global FeatureMap interface. The Feature string-literal type is derived as keyof FeatureMap. Register your app's features by augmenting FeatureMap from a declaration file in your project — do not redeclare Feature directly, as that would clash with the type shipped by this package and produce a Duplicate identifier 'Feature' error.

// app/types/features.d.ts
declare global {
  interface FeatureMap {
    beta: true;
    'experimental-search': true;
    'new-checkout': true;
  }
}

export {};

After augmenting, Feature resolves to 'beta' | 'experimental-search' | 'new-checkout' everywhere the package is consumed. If you don't augment FeatureMap, Feature is never and calls like isFeatureEnabled(...) won't type-check.

Usage

Bootstrap: register the config once

Statically import the generated config/app-config.json and hand it to setConfig at app startup. The same snippet works in a browser entry, a Node script, or an SSR server — there is no node:fs involved.

// app/setup.ts
import appConfig from '../config/app-config.json' with { type: 'json' };
import { setConfig } from '@jabraf/app-config';

setConfig(appConfig);

TypeScript: make sure "resolveJsonModule": true is set in your tsconfig.json (it is by default in @jabraf/dev / Vite / Next.js).

CommonJS bundlers / older Node: drop the with { type: 'json' } assertion — import appConfig from '../config/app-config.json' is enough.

First-time checkouts: since config/app-config.json is git-ignored and generated by buildConfig, the import may not resolve on a fresh clone. Either commit a placeholder {}, run buildConfig before the type-checker, or annotate the import with // @ts-expect-error generated by buildConfig.

Read configuration

import { getConfig } from '@jabraf/app-config';

const config = getConfig();
console.log(config.env);
console.log(config.hostname);
console.log(config.features);

getConfig returns whatever object was last registered with setConfig. It throws Error: app-config not set. Call setConfig(config) with your imported config/app-config.json at app startup. when called before bootstrap.

resetConfig() clears the registered config — primarily useful in tests.

Build configuration (Node only)

import { buildConfig } from '@jabraf/app-config/build';

await buildConfig();

buildConfig:

  1. Registers the tsx ESM loader so the project-owned config/make-config.ts can be imported at runtime without a separate compile step.
  2. Dynamically imports config/make-config.ts and reads its buildAppConfig export. If the export is missing, it throws Error: export buildAppConfig method from config/make-config.ts.
  3. Resolves the environment from process.env.APP_ENV, falling back to process.env.NODE_ENV, then defaulting to 'production'.
  4. Invokes buildAppConfig(env) and validates the result. If it returns null / undefined, it throws App Config could not be found. Make sure to return env specific config from buildAppConfig method in config/make-config.ts.
  5. Writes the returned config to config/app-config.json via fs.writeFileSync using JSON.stringify.

You typically don't call buildConfig directly — invoke it via the jabraf-config build CLI command instead (see CLI).

React: useFeature

Once setConfig has run, the useFeature hook reads features directly from the registered config:

// app/components/beta-banner.tsx
import { useFeature } from '@jabraf/app-config/hooks/use-feature.js';

function BetaBanner() {
  const { isFeatureEnabled } = useFeature();
  if (!isFeatureEnabled('beta')) return null;
  return <div>Welcome to the beta!</div>;
}

Cookies named feature__{feature} take precedence over the registered features, which makes runtime overrides (e.g., for QA or beta testers) easy.

Toggling features via cookies

import { setFeatureCookie, removeFeatureCookie } from '@jabraf/app-config';

setFeatureCookie('beta', true);
setFeatureCookie('beta', false);
removeFeatureCookie('beta');

Both setFeatureCookie and removeFeatureCookie are no-ops in SSR environments where document is undefined.

API Reference

setConfig(config: AppConfig): void

Registers the application configuration. Call once at app bootstrap with the statically-imported config/app-config.json. Subsequent calls replace the registered config.

getConfig(): AppConfig

Returns the registered AppConfig reference. Throws Error: app-config not set. Call setConfig(config) with your imported config/app-config.json at app startup. when called before setConfig.

resetConfig(): void

Clears the registered config so that the next getConfig call throws again. Intended for tests.

buildConfig(importer?): Promise<void>@jabraf/app-config/build

Node only. Registers the tsx ESM loader, dynamically imports config/make-config.ts from the project root, invokes its buildAppConfig(env) export, and writes the resolved config to config/app-config.json. The env is resolved from APP_ENV || NODE_ENV || 'production'. Throws when the export is missing or when buildAppConfig returns a nullish value. Accepts an optional Importer override — (specifier: string) => Promise<{ buildAppConfig: BuildAppConfig }> — primarily useful in tests.

useFeature()@jabraf/app-config/hooks/use-feature.js

React hook. Returns { isFeatureEnabled: (feature: Feature) => boolean }. Resolves features from getConfig().features. The returned function checks a feature__{name} cookie first and falls back to the registered features. The function is memoized against the registered features reference.

setFeatureCookie(feature: Feature, enabled: boolean): void@jabraf/app-config/hooks/use-feature.js

Writes feature__{feature}={enabled} with a one-year expiry and path=/. No-op when document is undefined.

removeFeatureCookie(feature: Feature): void@jabraf/app-config/hooks/use-feature.js

Clears the cookie for feature by setting it with an expiry in the past. No-op when document is undefined.

Utilities — @jabraf/app-config/build (Node only)

  • appDirectory(): string — Resolves the directory of the nearest package.json to the current working directory. Memoized after the first call.
  • fromRoot(...segments: string[]): string — Joins path segments against appDirectory().

Types

type Environment = 'dev' | 'test' | 'staging' | 'production';

type EnvironmentConfig<T> = {
  [key in Environment]: T;
};

type FeatureConfig<T extends string = Feature> = {
  [key in T]: boolean;
};

type AppConfig = {
  env: Environment;
  hostname: string;
  features: FeatureConfig;
};

type BuildAppConfig = (env: Environment) => Promise<AppConfig | null>;

CLI

The package ships a jabraf-config binary backed by commander.

# Print the usage hint
npx jabraf-config

# Build config/app-config.json from config/make-config.ts
npx jabraf-config build

# Build with an explicit environment
APP_ENV=staging npx jabraf-config build

# Build once, then watch config/ and rebuild on changes
npx jabraf-config build --watch
npx jabraf-config build -w

jabraf-config build is a thin wrapper around buildConfig() from @jabraf/app-config/build. The default action (no subcommand) prints Use "jabraf-config build" to build the app config..

--watch / -w

Performs an initial build, then watches the project's config/ directory recursively and rebuilds whenever a source file changes. File-change bursts are debounced (≈100 ms) and writes to the generated config/app-config.json are ignored so the watcher does not retrigger itself. Overlapping change events are coalesced into a single follow-up rebuild while one is in flight. A failed rebuild logs the error and keeps the watcher alive — fix the underlying file and save again to recover.

Stop the watcher with Ctrl+C.

$ npx jabraf-config build --watch
Watching config/ for changes. Press Ctrl+C to exit.
Rebuilt app config.
Rebuilt app config.

Consumer recipes

Standalone watch (no dev server)

Useful while iterating on config/make-config.ts itself or generating fixtures before running tests.

// package.json
{
  "scripts": {
    "config:build": "jabraf-config build",
    "config:watch": "jabraf-config build --watch",
  },
}
npm run config:watch

Alongside a Vite dev server

Run the watcher and the dev server in parallel so edits to config/make-config.ts regenerate config/app-config.json while Vite picks up the change via its own HMR. The watcher is debounced and skips its own output file, so it will not loop with the dev server's reloads.

Install concurrently (or npm-run-all) as a dev dependency:

npm install --save-dev concurrently
// package.json
{
  "scripts": {
    "config:build": "jabraf-config build",
    "config:watch": "jabraf-config build --watch",
    "dev:vite": "vite",
    "dev": "concurrently -k -n config,vite -c blue,green \"npm:config:watch\" \"npm:dev:vite\"",
  },
}
npm run dev

Notes for any dev server integration (Vite, Next.js, Remix, etc.):

  • Run jabraf-config build once before the dev server starts (e.g. via a predev script or as the first step of a composite dev task) so the statically imported config/app-config.json always exists.
  • Treat config/app-config.json as a build artifact: keep it in .gitignore and let the watcher own it. The watcher ignores writes to that file specifically, so it will not rebuild in response to its own output.
  • Run the watcher in the foreground next to your dev server so its logs (Rebuilt app config. / Failed to rebuild app config:) are visible — failures keep the watcher running but the previous JSON output remains in place until the next successful rebuild.
  • Set APP_ENV (or rely on NODE_ENV) the same way for both the one-shot build and the watcher to keep the generated config consistent: APP_ENV=dev concurrently ....

CI / one-shot builds

In CI or production builds, omit --watch so the command exits after a single build:

// package.json
{
  "scripts": {
    "prebuild": "jabraf-config build",
    "build": "vite build",
  },
}

License

MIT © Jabran Rafique