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 πŸ™

Β© 2024 – Pkg Stats / Ryan Hefner

next-flag

v1.3.0

Published

🏁 Feature flags powered by GitHub issues and NextJS. Toggle the features of your app by ticking a checkbox in a GitHub issue. Supports server-side rendering, multiple environments, and can be deployed as a stand-alone feature flag server.

Downloads

13

Readme

🏁 next-flag

Feature flags powered by GitHub issues and NextJS. Toggle the features of your app without deploying a new version by ticking a checkbox in the body of a GitHub issue.

✨ Features

  • [x] Enable or disable features by ticking a checkbox in a GitHub issue.
  • [x] Define feature flags across multiple environments or branches.
  • [x] Supports React Server Side and Client Side Components. Powered by the NextJS Cache.
  • [x] Define custom conditions that are evaluated at runtime to enable or disable features.
  • [x] Can be deployed as a stand-alone service to manage feature flags for multiple NextJS apps.

Check-out a fully working NextJS example or jump to Getting started.

πŸ“‘ Install

npm install next-flag

yarn add next-flag

pnpm add next-flag

πŸ‘‹ Hello there! Follow me @linesofcode or visit linesofcode.dev for more cool projects like this one.

πŸ—οΈ Architecture

architecture

πŸŽ₯ Demo

https://github.com/TimMikeladze/next-flag/assets/702718/d1707ae0-f9cf-4f80-a20f-6a6f9f715dc2

πŸš€ Getting started

πŸ“„ Create a new issue

First, create a new issue in your repository with the following format. It is optional to include a list of environments that the feature should be enabled in.

# 🏁 Feature Flags

## WIP feature

- [x] Enabled

## New feature

- [x] Enabled

### Production

- [ ] Enabled

### Preview

- [ ] Enabled

### Development

- [ ] Enabled

πŸ™ Setup GitHub

Now let's get an auth token from GitHub and create a Webhook.

  1. Create a new personal access token in GitHub with Read access to issues and metadata.
  2. Create a GitHub Webhook by navigating to https://github.com/<OWNER>/<REPO>/settings/hooks/new
    • Set the Payload URL to https://<YOUR_DOMAIN>/api/next-flag. Hint: Use ngrok for local development.
    • Set the Content type to application/json
    • Set the Secret to a random string
    • Select the Issues event.
  3. Add the GitHub token and webhook secret to the .env file of your NextJS app.
NEXT_FLAG_GITHUB_TOKEN=""
NEXT_FLAG_WEBHOOK_SECRET=""

πŸ’» Configure your NextJS app

Finally, let's write some code to use the next-flag package.

// src/app/api/next-flag/index.ts
import { NextFlag } from 'next-flag';
import { revalidateTag, unstable_cache } from 'next/cache';

export const nf = new NextFlag({
  paths: [
    {
      repository: '<OWNER>/<REPO>',
      issue: 123,
    },
  ],
  cache: {
    revalidateTag,
    unstable_cache,
  },
});

Next, create a new API route to handle the incoming Webhook requests.

// src/app/api/next-flag/route.ts
import { NextRequest } from 'next/server';

import { nf } from '.';

export const POST = (req: NextRequest) => nf.POST(req);

export const GET = (req: NextRequest) => nf.GET(req);

You can now use the nf instance to check if a feature is enabled in your app.

This can be done in a React Server Component:

// src/app/page.tsx
'use server';

import { nf } from './api/next-flag';

export default async function Page() {
  const wipFeatureEnabled = await nf.isFeatureEnabled('wip-feature');

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
}

Or in a React Client Component:

// src/app/components/Feature.tsx
'use client';

import { useNextFlag } from 'next-flag/react';

export const Feature = () => {
  const nf = useNextFlag();

  if (nf.loading) {
    return null;
  }

  const wipFeatureEnabled = nf.isFeatureEnabled('wip-feature');

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
};

You can also wrap your client side app with the NextFlagProvider to fetch features once on mount and provide them to child components when using the useNextFlag hook.

// src/app/components/Feature.tsx
'use client';

import { NextFlagProvider, useNextFlag } from 'next-flag/react';

const ContextProvider = () => {
  return (
    <NextFlagProvider>
      <Component />
    </NextFlagProvider>
  );
};

const Component = () => {
  const nf = useNextFlag();
  const gettingStarted = nf.isFeatureEnabled('getting-started');
  return (
    <>
      {gettingStarted && (
        <p>
          Get started by editing&nbsp;
          <code className={styles.code}>src/app/page.tsx</code>
        </p>
      )}
    </>
  );
};

πŸ’ͺ Advanced Usage

🚦 Conditions

Each feature flag can have a list of conditions that must be met for the feature to be enabled. Conditions are defined as a list of expressions that are evaluated at runtime. If any of the expressions return false, the feature will be disabled.

To get started, add a #### Conditions subheading to the feature issue and list the conditions as a series of checkboxes. If all conditions are met, the feature will be enabled. If a condition checkbox is unchecked, it will be ignored during evaluation. In other words, if a condition checkbox is not checked, it will not affect the feature flag.

# 🏁 Feature Flags

## My feature

- [x] Enabled

#### Conditions

- [ ] Only if admin

Now define how the condition is evaluated during runtime.

  1. Define a requestToContext function that takes a request object and returns a context object. The context object is passed to the condition functions.
  2. For each path, define a condition function that takes the context object and returns a boolean.

The requestToContext is a good place to extract information from the request object that is needed to evaluate the conditions. For example, you can extract cookies or headers from the request object to determine if a user is signed in.

❗ Important: The requestToContext function is only called when communicating with the NextFlag API over HTTP. If you are using the NextFlag directly in a server-side component, you must build the context object yourself and pass it to the isFeatureEnabled method directly.

// src/app/api/next-flag/index.ts
import { NextFlag } from 'next-flag';
import { revalidateTag, unstable_cache } from 'next/cache';

export const nf = new NextFlag({
  paths: [
    {
      repository: 'TimMikeladze/next-flag',
      issue: 3,
      conditions: {
        'only-if-admin': (context) => context.isAdmin,
      },
    },
  ],
  async requestToContext(req) {
    return {
      isAdmin: false,
    };
  },
  cache: {
    revalidateTag,
    unstable_cache,
  },
});
// src/app/page.tsx
'use server';

import { nf } from './api/next-flag';

export default async function Page() {
  const wipFeatureEnabled = await nf.isFeatureEnabled('wip-feature', {
    context: {
      isAdmin: true,
    }
  });

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
}

When using next-flag in a server-side component, you can also pass an and or or async function to the isFeatureEnabled method options to define extra in-line conditions that must be met for the feature to be enabled.

// src/app/page.tsx
'use server';

export default async function Page() {
  const wipFeatureEnabled = await nf.isFeatureEnabled('wip-feature', {
    and: () => true,
  });

  return wipFeatureEnabled && <div>WIP feature enabled!</div>;
}

🏝️ Multiple environments or branches

By default next-flag will try to read process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.NEXT_PUBLIC_ENV || process.env.NEXT_PUBLIC_STAGE || process.env.VERCEL_ENV || process.env.ENV || process.env.STAGE || process.env.NODE_ENV to determine the current environment.

You can customize how the current environment is determined during runtime by passing a getEnvironment function to the NextFlag constructor.

To associate a feature with a specific environment, add a subheading to the feature issue with the name of the environment (case-insensitive).

  • When using multiple environments, the top-level feature flag will control whether the feature is enabled or disabled.
  • If the top-level feature flag is disabled, the feature will be disabled in all environments.
  • If the top-level feature flag is enabled, then the environment-specific flags will determine whether the feature is enabled.
# 🏁 Feature Flags

## My feature

- [x] Enabled

### Production

- [ ] Enabled

### Preview

- [ ] Enabled

### Development

- [ ] Enabled

βœ… Getting all features

You can always get all features by calling the getFeatures method. You can also open the /api/next-flag route in your browser to see the enabled features as a JSON array.

import { nf } from './api/next-flag';
import { getFeatures, isFeatureEnabled } from 'next-flag/client';

// server side
const features = await nf.getFeatures();
// or client side with an HTTP request
const features = await getFeatures();
// check if a feature is enabled
const wipFeatureEnabled = await isFeatureEnabled('wip-feature');

πŸ“¦ Deploying a stand-alone next-flag server

You can deploy the next-flag server as a separate NextJS app and use it as a feature flagging service for multiple NextJS apps.

  1. Follow the steps above to setup GitHub and create a new NextJS app.
  2. When initializing the NextFlag instance, pass multiple projects to the paths option and set standalone to true.
  3. Deploy this NextJS app somewhere...
  4. In a different NextJS app:
    1. Configure the .env file with a NEXT_PUBLIC_NEXT_FLAG_PROJECT and NEXT_PUBLIC_NEXT_FLAG_ENDPOINT.
    2. Use isFeatureEnabled from the next-flag/client package to check if a feature is enabled in a React Server Component.
    3. Use the useNextFlag hook from the next-flag/react package to check if a feature is enabled in a React Client Component.
NEXT_PUBLIC_NEXT_FLAG_PROJECT="project-1"
NEXT_PUBLIC_NEXT_FLAG_ENDPOINT="https://<YOUR_DOMAIN>/api/next-flag"
// src/app/api/next-flag/index.ts
import { NextFlag } from 'next-flag';
import { revalidateTag, unstable_cache } from 'next/cache';

export const nf = new NextFlag({
  standalone: true,
  paths: [
    {
      project: 'project-1',
      repository: '<OWNER>/<REPO>',
      issue: 123,
    },
    {
      project: 'project-2',
      repository: '<OWNER>/<REPO>',
      issue: 124,
    },
  ],
  cache: {
    revalidateTag,
    unstable_cache,
  },
});

When running in stand-alone mode with multiple projects, you can pass the project option to the isFeatureEnabled method to check if a feature is enabled in a specific project.

You can pass an environment option to the isFeatureEnabled method to check if a feature is enabled in a specific environment.

These options will override the default values pulled from the environment variables.

import { isFeatureEnabled, getFeatures } from 'next-flag/client';

await isFeatureEnabled('wip-feature', {
  project: 'project-1',
  environment: 'development',
});

⛔️ Usage without a Webhook

Using a Github Webhook is optional, but highly recommended. The webhook is responsible for invalidating the NextJS Cache. Without this mechanism, caching of feature flags will be disabled and the feature flags will be fetched on every request.

If you don't want to use a Webhook simply omit the NEXT_FLAG_WEBHOOK_SECRET from the .env file.

πŸ“š TSDoc

:toolbox: Functions

:gear: getFeatures

| Function | Type | | ------------- | --------------------------------------------------- | | getFeatures | (props?: GetFeaturesArgs) => Promise<GetFeatures> |

:gear: isFeatureEnabled

| Function | Type | | ------------------ | -------------------------------------------------------------------------------------- | | isFeatureEnabled | (feature: string or string[], options?: IsFeatureEnabledOptions) => Promise<boolean> |

:gear: useNextFlag

| Function | Type | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | useNextFlag | (props?: UseNextFlagHookProps) => { loading: boolean; features: GetFeatures; error: Error or undefined; isFeatureEnabled: (feature: string or string[]) => boolean; } |

:gear: NextFlagProvider

| Function | Type | | ------------------ | --------------------------------------------------- | | NextFlagProvider | (props: NextFlagProviderProps) => Element or null |

:wrench: Constants

:gear: NextFlagContext

| Constant | Type | | ----------------- | ----------------------------------- | | NextFlagContext | Context<GetFeatures or undefined> |

:factory: NextFlag

Methods

:gear: GET

| Method | Type | | ------ | ---------------------------------------------------------- | | GET | (req: NextRequest) => Promise<NextResponse<GetFeatures>> |

:gear: isFeatureEnabled

| Method | Type | | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | isFeatureEnabled | (feature: string or string[], options?: { and?: (() => boolean or Promise<boolean>) or undefined; context?: Context or undefined; environment?: string or undefined; or?: (() => boolean or Promise<...>) or undefined; project?: string or undefined; }) => Promise<...> |

:gear: getFeatures

| Method | Type | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | getFeatures | (options?: { context?: Context or undefined; environment?: string or undefined; project?: string or undefined; }) => Promise<GetFeatures> |

:gear: POST

| Method | Type | | ------ | -------------------------------------------------------------------------------------------------------- | | POST | (req: NextRequest) => Promise<NextResponse<{ error: string; }> or NextResponse<{ success: boolean; }>> |