Skip to content

minichate/ember-cli-conditional-compile

Repository files navigation

ember-cli-conditional-compile

Build Status

The goal of ember-cli-conditional-compile is to provide easy to use feature switches to Ember applications such that code that is hidden behind a disabled feature switch is not in the compiled code.

Getting Started

This is an ember-cli addon, so all you need to do is

    ember install ember-cli-conditional-compile

Compatibility

Version 2.x targets Ember 3.x and beyond. It is tested against the latest ember release and the latest beta release (See config/ember-try.js for the testing matrix)

Upgrading to 2.x

Our workaround around a broccoli pipeline bug in newer versions of ember-cli slightly changes the semantics of includeDirByFlag. Instead of a RegExp, you now need to specify a Glob-style pattern.

Usage

To actually use the feature switches you'll need to add some configuration in your environment.js file. For example, lets pretend you want to have two feature switches; ENABLE_FOO and ENABLE_BAR:

var ENV = {
  // other settings ...

  featureFlags: {
    ENABLE_FOO: true,
    ENABLE_BAR: true,
  },
  includeDirByFlag: {
    ENABLE_FOO: ["pods/foos/**", "pods/foo/**"],
    ENABLE_BAR: [],
  },
};

// other environments ...

if (environment === "production") {
  ENV.featureFlags.ENABLE_FOO = false;
}

Alternatively, you can define your feature flags in config/feature-flags.js looking like this:

module.exports = function (environment) {
  const GLOBAL_FLAGS = {
    featureFlags: {
      ENABLE_FOO: true,
      ENABLE_BAR: true,
    },
    includeDirByFlag: {
      ENABLE_FOO: [/pods\/foos/, /pods\/foo/],
      ENABLE_BAR: [],
    },
  };

  if (environment === "production") {
    GLOBAL_FLAGS.featureFlags.ENABLE_FOO = false;
  }

  return GLOBAL_FLAGS;
};

This has two advantages: It declutters environment.js a bit, especially if you have many flags, but also prevents your flag names from leaking into the application code under certain circumstances.

We'll look at the two new options in more detail below, but for now we can see that by default both features are enabled, but in the production environment ENABLE_FOO is disabled, and related code under the pods/foos and pods/foo directories are excluded from compilation.

ENV.featureFlags

This setting sets up which flags will be available to actually switch use. A value of true means that the flag will be enabled, false means that it will not be.

ENV.includeDirByFlag

Given a key which has been defined above, the value is an array of regexes of files/paths which will only be included in the compiled product if the related feature flag is enabled. In the example above, in the development environment ENABLE_FOO is true, so the pods/foo and pods/foos paths will be included.

However, since the flag is false in production, any code in those directories will not be compiled in.

How it works

ember-cli-conditional-compile adds itself to the Broccoli compile pipeline for your Ember application. Depending on which environment you're building it acts in two different ways:

Development and test environments

Global variables are injected into the page which have the current state of the feature flags. For example:

if (ENABLE_FOO) {
  this.route("foo");
  console.log("The feature ENABLE_FOO is enabled in this environment");
}

will be represented in development and test environments as:

window.ENABLE_FOO = true;

if (ENABLE_FOO) {
  this.route("foo");
  console.log("The feature ENABLE_FOO is enabled in this environment");
}

In Handlebars/HTMLBars templates, you can also make use of the flags using the if-flag block helper:

{{#if-flag ENABLE_FOO}}
  <p>Foo is enabled! \o/</p>
{{else}}
  <p>Foo is disabled</p>
{{/if-flag}}

You can also use the unless-flag style block helper:

{{#unless-flag ENABLE_FOO}}
  <p>Foo is disabled</p>
{{else}}
  <p>Foo is enabled! \o/</p>
{{/unless-flag}}

Production environment

We use UglifyJS's global_defs feature to replace the value of feature flags with their constant values. UglifyJS's dead code implementation then cleans up unreachable code and performs inlining, such that:

if (ENABLE_FOO) {
  this.route("foo");
  console.log("The feature ENABLE_FOO is enabled in this environment");
}

will be represented in the production environment as the following if ENABLE_FOO is configured to be true:

this.route("foo");
console.log("The feature ENABLE_FOO is enabled in this environment");

or the following if ENABLE_FOO is configured to be false;

// empty since the condition can never be satisfied!

Furthermore, if you use the HTMLBars helpers the AST transformations will shake out and remove impossible-to-reach sides of the condition:

{{#if-flag ENABLE_FOO}}
  <p>Foo is enabled</p>
{{else}}
  <p>This won't be reached, because ENABLE_FOO is true</p>
{{/if-flag}}

will get transformed into:

<p>Foo is enabled</p>

This is really handy, since it vastly cuts down on the amount of precompiled template code that your users need to download even though it'll never be executed!

Defining additional environments

By defining ENV.featureFlagsEnvironment you can separate your feature flags by more than just test/development/production, for example to have a beta environment that is identical to production but has a couple more flags activated. This only works if you have your flags in config.featureFlags - The environment passed in into the wrapper function will be ENV.featureFlagsEnvironment if set.

Licence

This library is lovingly brought to you by the FreshBooks developers. We've released it under the MIT license.