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

WIP: Implement feature flags for Search Block #13829

Closed
wants to merge 12 commits into from

Conversation

talldan
Copy link
Contributor

@talldan talldan commented Feb 12, 2019

Description

This is an offshoot of #13324, an attempt to implement feature flags from that PR for the new Search Block.

How has this been tested?

Screenshots

Types of changes

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.

/>
</div>
);
if ( process.env.GUTENBERG_PHASE === 2 ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this here? Isn't the check in the block-library sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ternary in block-library/index.js is enough to make sure the block isn't registed, the other if statements make sure the code is removed from the bundle if the phase isn't 2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To add to that, my understanding is the other code (search/edit, search/index) would be eliminated automatically if we started using the sideEffects property in the package.json of our packages:
https://webpack.js.org/guides/tree-shaking/

The new webpack 4 release expands on this capability with a way to provide hints to the compiler via the "sideEffects" package.json property to denote which files in your project are "pure" and therefore safe to prune if unused.

I suppose it's a question of how far we want to go. If simply removing code from the public api is the goal, then it becomes a lot simpler to implement.

If the code needs to be eliminated entirely from the bundle, then the extra logic is needed in each file or we can try using sideEffects.

Copy link
Member

@gziolo gziolo Feb 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blowery or @jsnajdr, I remember you talking about it in one of the PRs. What do you recommend?

There are no blockers to mark @wordpress/block-library with sideEffects: false. That would be much more convenient.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried with sideEffects disabled and it works great. I can't find any JS code inside bundled production file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, specifying sideEffects: false and doing the process.env.GUTENBERG_PHASE === 2 check only in the block library is the best way forward.

Isn't the check in the block-library sufficient?

Without sideEffect: false, the search block would still be imported and bundled. The transformed code would be:

import * as search from './search';

registerBlocks( [ false ? search : undefined ] );

The fact that the search import is not used is not a sufficient reason to eliminate it. The ./search module's code can have side effects (e.g., window.wp.search = { ... }) that need to be executed. The sideEffects: false flag is needed to tell webpack that the elimination is safe and desired.

By the way, the way how each block's name and settings are exported as distinct named exports and then treated as one object (import * as search) was surprising to see. It would be more natural to treat the whole { name, settings } object as one unit and export is as default. Or is there any case where the exports are cherry-picked, like in:

import { name as searchName } from '@wordpress/block-library/search';

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, the way how each block's name and settings are exported as distinct named exports and then treated as one object (import * as search) was surprising to see. It would be more natural to treat the whole { name, settings } object as one unit and export is as default. Or is there any case where the exports are cherry-picked, like in:

import { name as searchName } from '@wordpress/block-library/search';

?

Agreed, I think I coded it this way a long time ago and I don't remember why :) We are going to change it in the near future as we are finalizing how blocks can be registered on the server and client using shared settings with JSON definition. See #13693. There are going to be new challenges :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many thanks for explaining how Webpack uses sideEffects to eliminate dead code. I will investigate which packages can benefit from it. There are lots of packages which don't run any side-effects when they are loaded in the browser. I think the only issue we will have with those which register stores.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only issue we will have with those which register stores.

That's right, we don't need to go far to see a typical module side effect 🙂 The store registrations, namely lack of control over the registration order, caused a few headaches in the Calypso integration project by the way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, every call to registerStore seems to cause side effects. We'd have to list those files or find a way to not cause side-effects.

Block registration seems ok though.

I've made an issue here to track adding the property:
#13910


it( 'can be created by typing "/quote"', async () => {
// Create a list with the slash block shortcut.
await clickBlockAppender();
Copy link
Member

@gziolo gziolo Feb 14, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's try to use feature flag also inside the test and see if it works.

Add new package for editor configuration, initially containing just feature flags

Rework build commands to use correct NODE_ENV for feature flags

Revert "Rework build commands to use correct NODE_ENV for feature flags"

This reverts commit 4cb0a39.

Revert "Add new package for editor configuration, initially containing just feature flags"

This reverts commit 0c21fc2.

Switch to using webpack define plugin to inject a global GUTENBERG_PHASE variable

Iterate: use window.GUTENBERG_PHASE to avoid thrown errors from an undefined global

Add custom eslint rule for usage of GUTENBERG_PHASE

Disable new eslint rule when used in webpack config

Add readme

Include phase 2 features in e2e tests

Allow use of GUTENBERG_PHASE in a ternary and update documentation.

Add links to docs

Minor docs changes

Switch from window.GUTENBERG_PHASE to process.env.GUTENBERG_PHASE

Update docs for feature flags. Move `Basic Use` section higher up, and simplify a sentence
@talldan talldan force-pushed the try/use-feature-flags-with-a-feature branch from e0b919c to e33dec5 Compare February 15, 2019 06:58
@talldan talldan force-pushed the try/use-feature-flags-with-a-feature branch from e33dec5 to 1e80885 Compare February 15, 2019 07:25

- Only access `GUTENBERG_PHASE` via `process.env`, e.g. `process.env.GUTENBERG_PHASE`. This is required since webpack's define plugin only replaces exact matches of `process.env.GUTENBERG_PHASE` in the codebase.
- The `GUTENBERG_PHASE` variable should only be used in a strict equality comparison with a number, e.g. `process.env.GUTENBERG_PHASE === 2` or `process.env.GUTENBERG_PHASE !== 2`. The value of the injected variable should always be a number, so this ensures the correct evaluation of the expression. Furthermore, when `process.env.GUTENBERG_PHASE` is undefined this comparison still returns either true (for `!==`) or false (for `===`), whereas both the `<` and `>` operators will always return false.
- `GUTENBERG_PHASE` should only be used within the condition of an if statement, e.g. `if ( process.env.GUTENBERG_PHASE === 2 ) { // implement feature here }` or ternary `process.env.GUTENBERG_PHASE === 2 ? something : somethingElse`. This rule ensures code that is disabled through a feature flag is removed by dead code elimination.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logical expressions are also supported and will work correctly:

process.env.GUTENBERG_PHASE === 2 && fun();
process.env.GUTENBERG_PHASE === 2 || fun();


When building the codebase for the plugin the variable will be replaced with the number literal `2`:
```js
if ( 2 === 2 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more technically correct, webpack will evaluate the whole === expression and will replace it with true. That's what you will see in the unminified output.

/>
</div>
);
if ( process.env.GUTENBERG_PHASE === 2 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, specifying sideEffects: false and doing the process.env.GUTENBERG_PHASE === 2 check only in the block library is the best way forward.

Isn't the check in the block-library sufficient?

Without sideEffect: false, the search block would still be imported and bundled. The transformed code would be:

import * as search from './search';

registerBlocks( [ false ? search : undefined ] );

The fact that the search import is not used is not a sufficient reason to eliminate it. The ./search module's code can have side effects (e.g., window.wp.search = { ... }) that need to be executed. The sideEffects: false flag is needed to tell webpack that the elimination is safe and desired.

By the way, the way how each block's name and settings are exported as distinct named exports and then treated as one object (import * as search) was surprising to see. It would be more natural to treat the whole { name, settings } object as one unit and export is as default. Or is there any case where the exports are cherry-picked, like in:

import { name as searchName } from '@wordpress/block-library/search';

?

```

```js
if ( true || process.env.GUTENBERG_PHASE > 2 ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of antipattern is this example trying to show?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Status] In Progress Tracking issues with work in progress
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants