Feature Flags

Summary

This RFC defines the infrastructure pieces to support feature flags to enable experimentation and progressive development inside LWC core packages.

Motivation

Goals of this proposal

Prior Art

EmberJS is probably one of the more successful frameworks when it comes to backwards-compatible changes. We can take a page from their playbook when it comes to introducing new features via Feature Flags:

Proposal

New features are added within conditional statements.

Code behind these flags can be conditionally enabled (or completely removed) based on your project configuration. This allows newly developed features to be selectively released when the LWC platform considers them ready for production use.

Flagging Details

The flag status in the generated build is controlled by the @lwc/features package. This package exports a list of all available features and their current status.

A feature can have one of a three flags:

The process of removing the feature flags from the resulting build output is handled by the LWC's build step.

When you need to use a feature flag?

If you intend to make "substantial" changes to LWC or any other package inside this repository or their documentation. What constitutes a "substantial" change is evolving based on community norms, but may include the following:

Examples of changes that would not require a feature flag:

Detailed Design

Using features in Code

In your code, you must import bindings from the features package to branch your code. We have two options:

If/Then/Else Branching

import { ENABLE_FOO } from "@lwc/features";
if (ENABLE_FOO) {
    runExtra();
}

which gets compiled to one of the following options:

import { ENABLE_FOO } from "@lwc/features";
// foo feature is configured as `true`
{ // this block is needed in case the then-block declares some binding
    runExtra();
}
import { ENABLE_FOO } from "@lwc/features";
// foo feature is configured as `false`
import { ENABLE_FOO } from "@lwc/features";
// foo feature is configured as `null`
if (globalThis.LWC_config.feature.ENABLE_FOO === true) {
    runExtra();
}

Arrow Function Branching

import { feature } from "@lwc/features";
feature('foo', _ => {
    // do something...
});

The output is the exact same as the previous option, but overall, this feature has one main benefit, it is a lot easier to identify, since the syntax is always the same.

Enabling At Build Time

TBD

Enabling At Runtime

This could be achieve via the global LWC configuration, e.g.:

// client side configuration
globalThis.LWC_config = {
    features: {
        foo: true,
        bar: false,
    },
};

All runtime-evaluated features are disabled by default. Therefore, features can only be enabled when the value is true.

It is important to notice that this configuration is a global variable, and must be evaluated before LWC runs.

It is also important to notice that only features that are configured as null on the LWC multi-package repository can be enabled or disabled on the client side. If the feature is marked as true, it will be always present independently of the LWC_config value, while those marked as false are just not present at runtime, which means attempting to configure those via LWC_config will do nothing.

Testing

During the test phase, all features will be compiled out as runtime optional (equivalent to setting them to null value). This guarantees that we can have different tests for the different branching logic. With that in mind, you can use a test helper function to enable certain features per test, e.g.:

import { enableFeature } from '@salesforce/lwc-test-support'; // bikeshed required here

enableFeature('foo'); // enabling feature foo for this test (we don't need a way to disable them)

describe('foo', () => {
    test('should ...', () => {
        // ...
    });
});

Note that this will only work for features behind flags that are not used during the initialization of the engine.

// Works when run as part of initialization
const foo = () => {
    if (ENABLE_FOO) {
        return 1;
    } else {
        return 2;
    }
};

// Doesn't work when run as part of initialization
let foo;
if (ENABLE_FOO) {
    foo = () => 1;
} else {
    foo = () => 2;
}

Extensibility

It is possible that @lwc/features can be used beyond this package, so other parts of the platform can implement a similar solution for their own features. This might include components, which means that the LWC compiler will have to understand the transformation as well.

Alternatives

TBD

How we teach this ?

TBD

undefined