Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add feature flags for Phase 2 #13324

Merged
merged 22 commits into from
Feb 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fb60a6c
Add new package for editor configuration, initially containing just f…
talldan Jan 15, 2019
3215406
Revert "Rework build commands to use correct NODE_ENV for feature flags"
talldan Jan 24, 2019
a8a3494
Switch to using webpack define plugin to inject a global GUTENBERG_PH…
talldan Jan 24, 2019
4d8f8f9
Iterate: use window.GUTENBERG_PHASE to avoid thrown errors from an un…
talldan Jan 25, 2019
116be55
Add custom eslint rule for usage of GUTENBERG_PHASE
talldan Jan 31, 2019
2116715
Disable new eslint rule when used in webpack config
talldan Feb 1, 2019
c678413
Add readme
talldan Feb 1, 2019
fb9768d
Include phase 2 features in e2e tests
talldan Feb 5, 2019
73f13dc
Allow use of GUTENBERG_PHASE in a ternary and update documentation.
talldan Feb 6, 2019
f395860
Add links to docs
talldan Feb 6, 2019
4be7f07
Minor docs changes
talldan Feb 6, 2019
ada6f4c
Switch from window.GUTENBERG_PHASE to process.env.GUTENBERG_PHASE
talldan Feb 11, 2019
ddce9f7
Update docs for feature flags. Move `Basic Use` section higher up, an…
talldan Feb 11, 2019
e91a027
Ensure GUTENBERG_PHASE environment variable is available for webpack
talldan Feb 12, 2019
7edd5c5
Ensure GUTENBERG_PHASE is a number
talldan Feb 12, 2019
6c93c7b
Ensure GUTENBERG_PHASE is set in unit tests
talldan Feb 12, 2019
806cdb8
Use <rootDir> in jest config
talldan Feb 13, 2019
235a770
switch to using package.json config to define the value of GUTENBERG_…
talldan Feb 14, 2019
ddc3853
Sort custom lint rules alphabetically
talldan Feb 15, 2019
94ffc01
Add comment about GUTENBERG_PHASE
talldan Feb 15, 2019
c78d6b4
Update jest config for GUTENBERG_PHASE
talldan Feb 15, 2019
57731f5
Add webpack as dependency to main package.json
talldan Feb 15, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions docs/designers-developers/developers/feature-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Feature Flags

With phase 2 of the Gutenberg project there's a need for improved control over how code changes are released. Newer features developed for phase 2 and beyond should only be released to the Gutenberg plugin, while improvements and bug fixes should still continue to make their way into core releases.

The technique for handling this is known as a 'feature flag'.

## Introducing `process.env.GUTENBERG_PHASE`

The `process.env.GUTENBERG_PHASE` is an environment variable containing a number that represents the phase. When the codebase is built for the plugin, this variable will be set to `2`. When building for core, it will be set to `1`.

## Basic Use

A phase 2 function or constant should be exported using the following ternary syntax:

```js
function myPhaseTwoFeature() {
// implementation
}

export const phaseTwoFeature = process.env.GUTENBERG_PHASE === 2 ? myPhaseTwoFeature : undefined;
```

In phase 1 environments the `phaseTwoFeature` export will be `undefined`.

If you're attempting to import and call a phase 2 feature, be sure to wrap the call to the function in an if statement to avoid an error:
```js
import { phaseTwoFeature } from '@wordpress/foo';

if ( process.env.GUTENBERG_PHASE === 2) {
phaseTwoFeature();
}
```

### How it works

During the webpack build, any instances of `process.env.GUTENBERG_PHASE` will be replaced using webpack's define plugin (https://webpack.js.org/plugins/define-plugin/).

If you write the following code:
```js
if ( process.env.GUTENBERG_PHASE === 2 ) {
phaseTwoFeature();
}
```

When building the codebase for the plugin the variable will be replaced with the number literal `2`:
```js
if ( 2 === 2 ) {
phaseTwoFeature();
}
```

Any code within the body of the if statement will be executed within the gutenberg plugin since `2 === 2` evaluates to `true`.

For core, the `process.env.GUTENBERG_PHASE` variable is replaced with `1`, so the built code will look like:
```js
if ( 1 === 2 ) {
phaseTwoFeature();
}
```

`1 === 2` evaluates to false so the phase 2 feature will not be executed within core.

### Dead Code Elimination

When building code for production, webpack 'minifies' code (https://en.wikipedia.org/wiki/Minification_(programming)), removing the amount of unnecessary JavaScript as much as possible. One of the steps involves something known as 'dead code elimination'.

When the following code is encountered, webpack determines that the surrounding `if`statement is unnecessary:
```js
if ( 2 === 2 ) {
phaseTwoFeature();
}
```

The condition will alway evaluates to `true`, so can be removed leaving just the code in the body:
```js
phaseTwoFeature();
```

Similarly when building for core, the condition in the following `if` statement always resolves to false:
```js
if ( 1 === 2 ) {
phaseTwoFeature();
}
```

The minification process will remove the entire `if` statement including the body, ensuring code destined for phase 2 is not included in the built JavaScript intended for core.

## FAQ

#### Why should I only use `===` or `!==` when comparing `process.env.GUTENBERG_PHASE` and not `>`, `>=`, `<` or `<=`?

This is a restriction due to the behaviour of the greater than or less than operators in JavaScript when `process.env.GUTENBERG_PHASE` is undefined, as might be the case for third party users of WordPress npm packages. Both `process.env.GUTENBERG_PHASE < 2` and `process.env.GUTENBERG_PHASE > 1` resolve to false. When writing `if ( process.env.GUTENBERG_PHASE > 1 )`, the intention might be to avoid executing the phase 2 code in the following `if` statement's body. That's fine since it will evaluate to false.

However, the following code doesn't quite have the intended behaviour:

```
function myPhaseTwoFeature() {
if ( process.env.GUTENBERG_PHASE < 2 ) {
return;
}

// implementation of phase 2 feature
}
```

Here an early return is used to avoid execution of a phase 2 feature, but because the `if` condition resolves to false, the early return is bypassed and the phase 2 feature is incorrectly triggered.

#### Why shouldn't I assign the result of an expression involving `GUTENBERG_PHASE` to a variable, e.g. `const isMyFeatureActive = process.env.GUTENBERG_PHASE === 2`?

The aim here is to avoid introducing any complexity that could result in webpack's minifier not being able to eliminate dead code. See the [Dead Code Elimination](#dead-code-elimination) section for further details.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@
"markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/accessibility.md",
"parent": "developers"
},
{
"title": "Feature Flags",
"slug": "feature-flags",
"markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/developers/feature-flags.md",
"parent": "developers"
},
{
"title": "Data Module Reference",
"slug": "data",
Expand Down
1 change: 1 addition & 0 deletions docs/toc.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
]},
{"docs/designers-developers/developers/internationalization.md": []},
{"docs/designers-developers/developers/accessibility.md": []},
{"docs/designers-developers/developers/feature-flags.md": []},
{"docs/designers-developers/developers/data/README.md": "{{data}}"},
{"docs/designers-developers/developers/packages.md": "{{packages}}"},
{"packages/components/README.md": "{{components}}"},
Expand Down
Loading