diff --git a/docs/designers-developers/developers/feature-flags.md b/docs/designers-developers/developers/feature-flags.md new file mode 100644 index 00000000000000..47a28d0fa42c39 --- /dev/null +++ b/docs/designers-developers/developers/feature-flags.md @@ -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. diff --git a/docs/manifest.json b/docs/manifest.json index 675c7034e52a1f..038b93e03f25ab 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -107,6 +107,12 @@ "markdown_source": "https://github.com/raw/WordPress/gutenberg/master/docs/designers-developers/developers/accessibility.md", "parent": "developers" }, + { + "title": "Feature Flags", + "slug": "feature-flags", + "markdown_source": "https://github.com/raw/WordPress/gutenberg/master/docs/designers-developers/developers/feature-flags.md", + "parent": "developers" + }, { "title": "Data Module Reference", "slug": "data", diff --git a/docs/toc.json b/docs/toc.json index d3e7b6048a130d..8d87e52bc89755 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -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}}"}, diff --git a/package-lock.json b/package-lock.json index e59039690658a6..69e25252f2a913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3177,9 +3177,9 @@ } }, "ajv-keywords": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", - "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", "dev": true }, "alphanum-sort": { @@ -4147,9 +4147,9 @@ "dev": true }, "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.0.tgz", + "integrity": "sha512-EgmjVLMn22z7eGGv3kcnHwSnJXmFHjISTY9E/S5lIcTD3Oxw05QTcBLNkJFzcb3cNueUdF/IN4U+d78V0zO8Hw==", "dev": true }, "bindings": { @@ -4761,26 +4761,573 @@ } }, "chokidar": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", - "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.1.tgz", + "integrity": "sha512-gfw3p2oQV2wEt+8VuMlNsPjCxDxvvgnm/kz+uATu805mWVF8IJN7uz9DN7iBz+RMJISmiVbCOBFs9qBGMjtPfQ==", "dev": true, "requires": { "anymatch": "^2.0.0", - "async-each": "^1.0.0", - "braces": "^2.3.0", - "fsevents": "^1.2.2", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", - "inherits": "^2.0.1", + "inherits": "^2.0.3", "is-binary-path": "^1.0.0", "is-glob": "^4.0.0", - "lodash.debounce": "^4.0.8", - "normalize-path": "^2.1.1", + "normalize-path": "^3.0.0", "path-is-absolute": "^1.0.0", - "readdirp": "^2.0.0", - "upath": "^1.0.5" + "readdirp": "^2.2.1", + "upath": "^1.1.0" }, "dependencies": { + "fsevents": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", + "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "dev": true, + "optional": true + }, + "semver": { + "version": "5.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "dev": true, + "optional": true + } + } + }, "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", @@ -4816,6 +5363,12 @@ "requires": { "is-extglob": "^2.1.1" } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true } } }, @@ -6772,9 +7325,9 @@ "dev": true }, "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -7365,9 +7918,9 @@ } }, "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", + "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", "dev": true }, "evp_bytestokey": { @@ -9652,9 +10205,9 @@ } }, "hash.js": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", - "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -12908,9 +13461,9 @@ } }, "loader-runner": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.3.0.tgz", - "integrity": "sha1-9IKuqC1UPgeSFwDVpG7yb9rGuKI=", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", + "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", "dev": true }, "loader-utils": { @@ -12979,12 +13532,6 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true - }, "lodash.escape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", @@ -13401,13 +13948,14 @@ "dev": true }, "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "requires": { "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "mdast-util-compact": { @@ -14013,9 +14561,9 @@ "dev": true }, "node-libs-browser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.1.0.tgz", - "integrity": "sha512-5AzFzdoIMb89hBGMZglEegffzgRg+ZFoUmisQ8HI4j1KDdpx13J0taNp2y9xPbur6W61gepGDDotGBVQ7mfUCg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.0.tgz", + "integrity": "sha512-5MQunG/oyOaBdttrL40dA7bUfPORLRWMUJLQtMg7nluxUvk5XwnLdL9twQHFAjRx/y7mIMkLKT9++qPbbk6BZA==", "dev": true, "requires": { "assert": "^1.1.1", @@ -14025,7 +14573,7 @@ "constants-browserify": "^1.0.0", "crypto-browserify": "^3.11.0", "domain-browser": "^1.1.1", - "events": "^1.0.0", + "events": "^3.0.0", "https-browserify": "^1.0.0", "os-browserify": "^0.3.0", "path-browserify": "0.0.0", @@ -14039,7 +14587,7 @@ "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", "url": "^0.11.0", - "util": "^0.10.3", + "util": "^0.11.0", "vm-browserify": "0.0.4" }, "dependencies": { @@ -15090,9 +15638,9 @@ } }, "pako": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", - "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", + "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==", "dev": true }, "parallel-transform": { @@ -15124,16 +15672,17 @@ } }, "parse-asn1": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", + "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", "dev": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", "create-hash": "^1.1.0", "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3" + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-entities": { @@ -15299,9 +15848,9 @@ } }, "pbkdf2": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", - "integrity": "sha512-y4CXP3thSxqf7c0qmOF+9UeOTrifiVTIM+u7NWlq+PRsHbr7r7dpCmvzrZxa96JJUNi0Y5w9VqG5ZNeCVMoDcA==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -17278,16 +17827,17 @@ "dev": true }, "public-encrypt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz", - "integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dev": true, "requires": { "bn.js": "^4.1.0", "browserify-rsa": "^4.0.0", "create-hash": "^1.1.0", "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1" + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "pump": { @@ -17835,15 +18385,14 @@ } }, "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" } }, "realpath-native": { @@ -18757,9 +19306,9 @@ } }, "schema-utils": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.5.tgz", - "integrity": "sha512-yYrjb9TX2k/J1Y5UNy3KYdZq10xhYcF8nMpAW6o3hy6Q8WSIEf9lJHG/ePnOBfziPM3fvQwfOwa13U/Fh8qTfA==", + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", "dev": true, "requires": { "ajv": "^6.1.0", @@ -18767,15 +19316,15 @@ }, "dependencies": { "ajv": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.2.tgz", - "integrity": "sha512-hOs7GfvI6tUI1LfZddH82ky6mOMyTuY0mk7kE2pWpmhhUSkumzaTO5vbVwij39MdwPQWCV4Zv57Eo06NtL/GVA==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.1" + "uri-js": "^4.2.2" } }, "fast-deep-equal": { @@ -18891,12 +19440,6 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, - "set-immediate-shim": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", - "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", - "dev": true - }, "set-value": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", @@ -19453,9 +19996,9 @@ "dev": true }, "stream-browserify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", - "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { "inherits": "~2.0.1", @@ -20594,9 +21137,9 @@ } }, "uglifyjs-webpack-plugin": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz", - "integrity": "sha512-1VicfKhCYHLS8m1DCApqBhoulnASsEoJ/BvpUpP4zoNAPpKzdH+ghk0olGJMmwX2/jprK2j3hAHdUbczBSy2FA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.3.0.tgz", + "integrity": "sha512-ovHIch0AMlxjD/97j9AYovZxG5wnHOPkL7T1GKochBADp/Zwc44pEWNqpKl1Loupp1WhFg7SlYmHZRUfdAacgw==", "dev": true, "requires": { "cacache": "^10.0.4", @@ -20944,9 +21487,9 @@ "dev": true }, "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", "dev": true, "requires": { "inherits": "2.0.3" @@ -21245,9 +21788,9 @@ }, "dependencies": { "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", diff --git a/package.json b/package.json index 07cd37a6a93641..758213c258bead 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "WordPress", "editor" ], + "config": { + "GUTENBERG_PHASE": 2 + }, "dependencies": { "@wordpress/a11y": "file:packages/a11y", "@wordpress/annotations": "file:packages/annotations", @@ -113,6 +116,7 @@ "sprintf-js": "1.1.1", "stylelint-config-wordpress": "13.1.0", "uuid": "3.3.2", + "webpack": "4.8.3", "webpack-bundle-analyzer": "3.0.2", "webpack-livereload-plugin": "2.1.1", "webpack-rtl-plugin": "github:yoavf/webpack-rtl-plugin#develop" diff --git a/packages/e2e-tests/config/gutenberg-phase.js b/packages/e2e-tests/config/gutenberg-phase.js new file mode 100644 index 00000000000000..1b6117b3236a6a --- /dev/null +++ b/packages/e2e-tests/config/gutenberg-phase.js @@ -0,0 +1,6 @@ +global.process.env = { + ...global.process.env, + // Inject the `GUTENBERG_PHASE` global, used for feature flagging. + // eslint-disable-next-line @wordpress/gutenberg-phase + GUTENBERG_PHASE: parseInt( process.env.npm_package_config_GUTENBERG_PHASE, 10 ), +}; diff --git a/packages/e2e-tests/jest.config.js b/packages/e2e-tests/jest.config.js index 9984044fd0ccd5..f241715197b97c 100644 --- a/packages/e2e-tests/jest.config.js +++ b/packages/e2e-tests/jest.config.js @@ -1,4 +1,7 @@ module.exports = { ...require( '@wordpress/scripts/config/jest-e2e.config' ), setupTestFrameworkScriptFile: '/config/setup-test-framework.js', + setupFiles: [ + '/config/gutenberg-phase.js', + ], }; diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index daf663e8157e90..4e3f6f84309962 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -9,6 +9,7 @@ - New Rule: [`@wordpress/no-unused-vars-before-return`](https://github.com/WordPress/gutenberg/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md) - New Rule: [`@wordpress/dependency-group`](https://github.com/WordPress/gutenberg/blob/master/packages/eslint-plugin/docs/rules/dependency-group.md) - New Rule: [`@wordpress/valid-sprintf`](https://github.com/WordPress/gutenberg/blob/master/packages/eslint-plugin/docs/rules/valid-sprintf.md) +- New Rule: [`@wordpress/gutenberg-phase`](https://github.com/WordPress/gutenberg/blob/master/packages/eslint-plugin/docs/rules/gutenberg-phase.md) ## 1.0.0 (2018-12-12) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 2af095ba3afecb..cd1ab687823cde 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -49,8 +49,9 @@ The granular rulesets will not define any environment globals. As such, if they Rule|Description ---|--- -[no-unused-vars-before-return](/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md)|Disallow assigning variable values if unused before a return [dependency-group](/packages/eslint-plugin/docs/rules/dependency-group.md)|Enforce dependencies docblocks formatting +[gutenberg-phase](docs/rules/gutenberg-phase.md)|Governs the use of the `process.env.GUTENBERG_PHASE` constant +[no-unused-vars-before-return](/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md)|Disallow assigning variable values if unused before a return [valid-sprintf](/packages/eslint-plugin/docs/rules/valid-sprintf.md)|Disallow assigning variable values if unused before a return ### Legacy diff --git a/packages/eslint-plugin/configs/custom.js b/packages/eslint-plugin/configs/custom.js index 7d79a3e055b18a..82ba39265eb43b 100644 --- a/packages/eslint-plugin/configs/custom.js +++ b/packages/eslint-plugin/configs/custom.js @@ -4,6 +4,7 @@ module.exports = { ], rules: { '@wordpress/dependency-group': 'error', + '@wordpress/gutenberg-phase': 'error', '@wordpress/no-unused-vars-before-return': 'error', '@wordpress/valid-sprintf': 'error', 'no-restricted-syntax': [ diff --git a/packages/eslint-plugin/docs/rules/gutenberg-phase.md b/packages/eslint-plugin/docs/rules/gutenberg-phase.md new file mode 100644 index 00000000000000..abe70575c9cfb5 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/gutenberg-phase.md @@ -0,0 +1,62 @@ +# The `GUTENBERG_PHASE` global (gutenberg-phase) + +To enable the use of feature flags in Gutenberg, the GUTENBERG_PHASE global constant was introduced. This constant is replaced with a number value at build time using webpack's define plugin. + +There are a few rules around using this constant: + +- 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. + + +## Rule details + +Examples of **incorrect** code for this rule: + +```js +if ( GUTENBERG_PHASE === 2 ) { + // implement feature here. +} +``` + +```js +if ( window[ 'GUTENBERG_PHASE' ] === 2 ) { + // implement feature here. +} +``` + +```js +if ( process.env.GUTENBERG_PHASE === '2' ) { + // implement feature here. +} +``` + +```js +if ( process.env.GUTENBERG_PHASE > 2 ) { + // implement feature here. +} +``` + +```js +if ( true || process.env.GUTENBERG_PHASE > 2 ) { + // implement feature here. +} +``` + +```js +const isMyFeatureActive = process.env.GUTENBERG_PHASE === 2; +``` + +Examples of **correct** code for this rule: + +```js +if ( process.env.GUTENBERG_PHASE === 2 ) { + // implement feature here. +} +``` + +```js +if ( process.env.GUTENBERG_PHASE !== 2 ) { + return; +} +``` diff --git a/packages/eslint-plugin/rules/__tests__/gutenberg-phase.js b/packages/eslint-plugin/rules/__tests__/gutenberg-phase.js new file mode 100644 index 00000000000000..579e127855090d --- /dev/null +++ b/packages/eslint-plugin/rules/__tests__/gutenberg-phase.js @@ -0,0 +1,60 @@ +/** + * External dependencies + */ +import { RuleTester } from 'eslint'; + +/** + * Internal dependencies + */ +import rule from '../gutenberg-phase'; + +const ruleTester = new RuleTester( { + parserOptions: { + ecmaVersion: 6, + }, +} ); + +const ACCESS_ERROR = 'The `GUTENBERG_PHASE` constant should be accessed using `process.env.GUTENBERG_PHASE`.'; +const EQUALITY_ERROR = 'The `GUTENBERG_PHASE` constant should only be used in a strict equality comparison with a primitive number.'; +const IF_ERROR = 'The `GUTENBERG_PHASE` constant should only be used as part of the condition in an if statement or ternary expression.'; + +ruleTester.run( 'gutenberg-phase', rule, { + valid: [ + { code: `if ( process.env.GUTENBERG_PHASE === 2 ) {}` }, + { code: `if ( process.env.GUTENBERG_PHASE !== 2 ) {}` }, + { code: `if ( 2 === process.env.GUTENBERG_PHASE ) {}` }, + { code: `if ( 2 !== process.env.GUTENBERG_PHASE ) {}` }, + { code: `const test = process.env.GUTENBERG_PHASE === 2 ? foo : bar` }, + { code: `const test = process.env.GUTENBERG_PHASE !== 2 ? foo : bar` }, + ], + invalid: [ + { + code: `if ( GUTENBERG_PHASE === 1 ) {}`, + errors: [ { message: ACCESS_ERROR } ], + }, + { + code: `if ( window[ 'GUTENBERG_PHASE' ] === 1 ) {}`, + errors: [ { message: ACCESS_ERROR } ], + }, + { + code: `if ( process.env.GUTENBERG_PHASE > 1 ) {}`, + errors: [ { message: EQUALITY_ERROR } ], + }, + { + code: `if ( process.env.GUTENBERG_PHASE === '2' ) {}`, + errors: [ { message: EQUALITY_ERROR } ], + }, + { + code: `if ( true ) { process.env.GUTENBERG_PHASE === 2 }`, + errors: [ { message: IF_ERROR } ], + }, + { + code: `if ( true || process.env.GUTENBERG_PHASE === 2 ) {}`, + errors: [ { message: IF_ERROR } ], + }, + { + code: `const isFeatureActive = process.env.GUTENBERG_PHASE === 2;`, + errors: [ { message: IF_ERROR } ], + }, + ], +} ); diff --git a/packages/eslint-plugin/rules/gutenberg-phase.js b/packages/eslint-plugin/rules/gutenberg-phase.js new file mode 100644 index 00000000000000..98c7a4216a4f73 --- /dev/null +++ b/packages/eslint-plugin/rules/gutenberg-phase.js @@ -0,0 +1,165 @@ +/** + * Traverse up through the chain of parent AST nodes returning the first parent + * the predicate returns a truthy value for. + * + * @param {Object} sourceNode The AST node to search from. + * @param {function} predicate A predicate invoked for each parent. + * + * @return {?Object } The first encountered parent node where the predicate + * returns a truthy value. + */ +function findParent( sourceNode, predicate ) { + if ( ! sourceNode.parent ) { + return; + } + + if ( predicate( sourceNode.parent ) ) { + return sourceNode.parent; + } + + return findParent( sourceNode.parent, predicate ); +} + +/** + * Tests whether the GUTENBERG_PHASE variable is accessed via + * `process.env.GUTENBERG_PHASE`. + * + * @example + * ```js + * // good + * if ( process.env.GUTENBERG_PHASE === 2 ) { + * + * // bad + * if ( GUTENBERG_PHASE === 2 ) { + * ``` + * + * @param {Object} node The GUTENBERG_PHASE identifier node. + * @param {Object} context The eslint context object. + */ +function testIsAccessedViaProcessEnv( node, context ) { + const parent = node.parent; + + if ( + parent && + parent.type === 'MemberExpression' && + context.getSource( parent ) === 'process.env.GUTENBERG_PHASE' + + ) { + return; + } + + context.report( + node, + 'The `GUTENBERG_PHASE` constant should be accessed using `process.env.GUTENBERG_PHASE`.', + ); +} + +/** + * Tests whether the GUTENBERG_PHASE variable is used in a strict binary + * equality expression in a comparison with a number, triggering a + * violation if not. + * + * @example + * ```js + * // good + * if ( process.env.GUTENBERG_PHASE === 2 ) { + * + * // bad + * if ( process.env.GUTENBERG_PHASE >= '2' ) { + * ``` + * + * @param {Object} node The GUTENBERG_PHASE identifier node. + * @param {Object} context The eslint context object. + */ +function testIsUsedInStrictBinaryExpression( node, context ) { + const parent = findParent( node, ( candidate ) => candidate.type === 'BinaryExpression' ); + + if ( parent ) { + const comparisonNode = node.parent.type === 'MemberExpression' ? node.parent : node; + + // Test for process.env.GUTENBERG_PHASE === or === process.env.GUTENBERG_PHASE + const hasCorrectOperator = [ '===', '!==' ].includes( parent.operator ); + const hasCorrectOperands = ( + ( parent.left === comparisonNode && typeof parent.right.value === 'number' ) || + ( parent.right === comparisonNode && typeof parent.left.value === 'number' ) + ); + + if ( hasCorrectOperator && hasCorrectOperands ) { + return; + } + } + + context.report( + node, + 'The `GUTENBERG_PHASE` constant should only be used in a strict equality comparison with a primitive number.', + ); +} + +/** + * Tests whether the GUTENBERG_PHASE variable is used as the condition for an + * if statement, triggering a violation if not. + * + * @example + * ```js + * // good + * if ( process.env.GUTENBERG_PHASE === 2 ) { + * + * // bad + * const isFeatureActive = process.env.GUTENBERG_PHASE === 2; + * ``` + * + * @param {Object} node The GUTENBERG_PHASE identifier node. + * @param {Object} context The eslint context object. + */ +function testIsUsedInIfOrTernary( node, context ) { + const conditionalParent = findParent( + node, + ( candidate ) => [ 'IfStatement', 'ConditionalExpression' ].includes( candidate.type ) + ); + const binaryParent = findParent( node, ( candidate ) => candidate.type === 'BinaryExpression' ); + + if ( conditionalParent && + binaryParent && + conditionalParent.test && + conditionalParent.test.start === binaryParent.start && + conditionalParent.test.end === binaryParent.end + ) { + return; + } + + context.report( + node, + 'The `GUTENBERG_PHASE` constant should only be used as part of the condition in an if statement or ternary expression.', + ); +} + +module.exports = { + meta: { + type: 'problem', + schema: [], + }, + create( context ) { + return { + Identifier( node ) { + // Bypass any identifiers with a node name different to `GUTENBERG_PHASE`. + if ( node.name !== 'GUTENBERG_PHASE' ) { + return; + } + + testIsAccessedViaProcessEnv( node, context ); + testIsUsedInStrictBinaryExpression( node, context ); + testIsUsedInIfOrTernary( node, context ); + }, + Literal( node ) { + // Bypass any identifiers with a node value different to `GUTENBERG_PHASE`. + if ( node.value !== 'GUTENBERG_PHASE' ) { + return; + } + + if ( node.parent && node.parent.type === 'MemberExpression' ) { + testIsAccessedViaProcessEnv( node, context ); + } + }, + }; + }, +}; diff --git a/test/unit/config/gutenberg-phase.js b/test/unit/config/gutenberg-phase.js new file mode 100644 index 00000000000000..1b6117b3236a6a --- /dev/null +++ b/test/unit/config/gutenberg-phase.js @@ -0,0 +1,6 @@ +global.process.env = { + ...global.process.env, + // Inject the `GUTENBERG_PHASE` global, used for feature flagging. + // eslint-disable-next-line @wordpress/gutenberg-phase + GUTENBERG_PHASE: parseInt( process.env.npm_package_config_GUTENBERG_PHASE, 10 ), +}; diff --git a/test/unit/jest.config.json b/test/unit/jest.config.json index ed9e9ebc52774c..3b7d0ca732a994 100644 --- a/test/unit/jest.config.json +++ b/test/unit/jest.config.json @@ -6,7 +6,8 @@ }, "preset": "@wordpress/jest-preset-default", "setupFiles": [ - "core-js/fn/symbol/async-iterator" + "core-js/fn/symbol/async-iterator", + "/test/unit/config/gutenberg-phase.js" ], "testURL": "http://localhost", "testPathIgnorePatterns": [ diff --git a/webpack.config.js b/webpack.config.js index 7d9877d7c15e3e..998cabd9c9ace6 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,7 @@ /** * External dependencies */ +const { DefinePlugin } = require( 'webpack' ); const WebpackRTLPlugin = require( 'webpack-rtl-plugin' ); const LiveReloadPlugin = require( 'webpack-livereload-plugin' ); const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); @@ -105,6 +106,11 @@ const config = { ], }, plugins: [ + new DefinePlugin( { + // Inject the `GUTENBERG_PHASE` global, used for feature flagging. + // eslint-disable-next-line @wordpress/gutenberg-phase + 'process.env.GUTENBERG_PHASE': JSON.stringify( parseInt( process.env.npm_package_config_GUTENBERG_PHASE, 10 ) || 1 ), + } ), // Create RTL files with a -rtl suffix new WebpackRTLPlugin( { suffix: '-rtl',