From 1a43e3f10a5b781e66a758771570e71d1fee9639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 10 Feb 2023 16:56:11 +0100 Subject: [PATCH 1/5] Revise the coding guidelines to distinguish between experimental APIs and private APIs --- docs/contributors/code/coding-guidelines.md | 241 ++++++++------------ 1 file changed, 92 insertions(+), 149 deletions(-) diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 281ace04b141cd..c0a4823b3549b3 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -114,32 +114,39 @@ Example: import VisualEditor from '../visual-editor'; ``` -### Experimental and Unstable APIs +### Experimental APIs -Experimental and unstable APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. +Experimental APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. _To External Consumers:_ -**There is no support commitment for experimental and unstable APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. +**There is no support commitment for experimental APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. _To Project Contributors:_ -An experimental or unstable API is named as such to communicate instability of a function whose interface is not yet finalized. Aside from references within the code, these APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. +An experimental API is named as such to communicate instability of a function whose interface is not yet finalized. Aside from references within the code, these APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. -An experimental or unstable function or object should be prefixed respectively using `__experimental` or `__unstable`. +An experimental function or object should be: + +* Prefixed respectively using `__experimental` +* Only exported in the Gutenberg plugin so it never becomes a part of WordPress core + +Here's an example: ```js -export { __experimentalDoExcitingExperimentalAction } from './api'; -export { __unstableDoTerribleAwfulAction } from './api'; +// Using IS_GUTENBERG_PLUGIN allows Webpack to exclude the +// experimental exports from WordPress core: +if ( IS_GUTENBERG_PLUGIN ) { + export { __experimentalDoExcitingExperimentalAction } from './api'; +} ``` -- An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. -- An **unstable API** is one which serves as a means to an end. It is not desired to ever be converted into a public API. - -In both cases, the API should be made stable or removed at the earliest opportunity. +An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. It should be made stable or removed at the earliest opportunity. While an experimental API may often stabilize into a publicly-available API, there is no guarantee that it will. The conversion to a stable API will inherently be considered a breaking change by the mere fact that the function name must be changed to remove the `__experimental` prefix. +Other prefixes were also used in the past, such as `__unstable` or `__internal`. + #### Experimental APIs merged into WordPress Core become a liability **Avoid introducing public experimental APIs.** @@ -152,95 +159,16 @@ They are a part of the WordPress public API and fall under the Removing them involves a deprecation process. It may be relatively easy for some APIs, but it may require effort and span multiple WordPress releases for others. -**Use private experimental APIs instead.** +**Use private APIs instead.** Make your experimental APIs private and don't expose them to WordPress extenders. This way they'll remain internal implementation details that can be changed or removed without a warning and without breaking WordPress plugins. -The tactical guidelines below will help you write code without introducing new experimental APIs. - -#### General guidelines - -Some `__experimental` functions are exported in _package A_ and only used in a single _package B_ and nowhere else. Consider removing such functions from _package A_ and making them private and non-exported members of _package B_. - -If your experimental API is only meant for the Gutenberg Plugin but not for the next WordPress major release, consider limiting the export to the plugin environment. For example, `@wordpress/components` could do that to receive early feedback about a new Component, but avoid bringing that component to WordPress core: - -```js -if ( IS_GUTENBERG_PLUGIN ) { - export { __experimentalFunction } from './private-apis'; -} -``` - -#### Replace experimental selectors with hooks - -Sometimes a non-exported React hook suffices as a substitute for introducing a new experimental selectors: - -```js -// Instead of this: -// selectors.js: -export function __unstableHasActiveBlockOverlayActive( state, parent ) { - /* ... */ -} -export function __unstableIsWithinBlockOverlay( state, clientId ) { - let parent = state.blocks.parents[ clientId ]; - while ( !! parent ) { - if ( __unstableHasActiveBlockOverlayActive( state, parent ) ) { - return true; - } - parent = state.blocks.parents[ parent ]; - } - return false; -} -// MyComponent.js: -function MyComponent( { clientId } ) { - const { __unstableIsWithinBlockOverlay } = useSelect( myStore ); - const isWithinBlockOverlay = __unstableIsWithinBlockOverlay( clientId ); - // ... -} - -// Consider this: -// MyComponent.js: -function hasActiveBlockOverlayActive( selectors, parent ) { - /* ... */ -} -function useIsWithinBlockOverlay( clientId ) { - return useSelect( ( select ) => { - const selectors = select( blockEditorStore ); - let parent = selectors.getBlockRootClientId( clientId ); - while ( !! parent ) { - if ( hasActiveBlockOverlayActive( selectors, parent ) ) { - return true; - } - parent = selectors.getBlockRootClientId( parent ); - } - return false; - } ); -} -function MyComponent( { clientId } ) { - const isWithinBlockOverlay = useIsWithinBlockOverlay( clientId ); - // ... -} -``` - -#### Dispatch experimental actions in thunks - -Turning an existing public action into a [thunk](/docs/how-to-guides/thunks.md) -enables dispatching private actions inline: - -```js -export function toggleFeature( scope, featureName ) { - return function ( { dispatch } ) { - dispatch( { type: '__experimental_BEFORE_TOGGLE' } ); - // ... - }; -} -``` - -#### Use the `lock()` and `unlock()` API from `@wordpress/private-apis` to privately export almost anything +#### Private exports are supported – use the `lock()` and `unlock()` API from `@wordpress/private-apis` -Each `@wordpress` package wanting to privately access or expose experimental APIs can +Each `@wordpress` package wanting to privately access or expose a private APIs can do so by opting-in to `@wordpress/private-apis`: ```js @@ -264,10 +192,10 @@ Once the package opted-in, you can use the `lock()` and `unlock()` utilities: export const publicObject = {}; // However, this string is internal and should not be publicly available: -const __experimentalString = '__experimental information'; +const privateString = 'private information'; // Solution: lock the string "inside" of the object: -lock( publicObject, __experimentalString ); +lock( publicObject, privateString ); // The string is not nested in the object and cannot be extracted from it: console.log( publicObject ); @@ -275,92 +203,91 @@ console.log( publicObject ); // The only way to access the string is by "unlocking" the object: console.log( unlock( publicObject ) ); -// "__experimental information" +// "private information" // lock() accepts all data types, not just strings: export const anotherObject = {}; -lock( anotherObject, function __experimentalFn() {} ); +lock( anotherObject, function privateFn() {} ); console.log( unlock( anotherObject ) ); -// function __experimentalFn() {} +// function privateFn() {} ``` Keep reading to learn how to use `lock()` and `unlock()` to avoid publicly exporting -different kinds of `__experimental` APIs. +different kinds of `private` APIs. -##### Experimental selectors and actions +##### Private selectors and actions You can attach private selectors and actions to a public store: ```js // In packages/package1/store.js: -import { experiments as dataExperiments } from '@wordpress/data'; -import { __experimentalHasContentRoleAttribute, ...selectors } from './selectors'; -import { __experimentalToggleFeature, ...actions } from './selectors'; -// The `lock` function is exported from the internal experiments.js file where +import { privateHasContentRoleAttribute, ...selectors } from './selectors'; +import { privateToggleFeature, ...actions } from './selectors'; +// The `lock` function is exported from the internal private-apis.js file where // the opt-in function was called. import { lock, unlock } from './private-apis'; export const store = registerStore(/* ... */); // Attach a private action to the exported store: unlock( store ).registerPrivateActions({ - __experimentalToggleFeature + privateToggleFeature } ); // Attach a private action to the exported store: unlock( store ).registerPrivateSelectors({ - __experimentalHasContentRoleAttribute + privateHasContentRoleAttribute } ); // In packages/package2/MyComponent.js: import { store } from '@wordpress/package1'; import { useSelect } from '@wordpress/data'; -// The `unlock` function is exported from the internal experiments.js file where +// The `unlock` function is exported from the internal private-apis.js file where // the opt-in function was called. import { unlock } from './private-apis'; function MyComponent() { const hasRole = useSelect( ( select ) => ( // Use the private selector: - unlock( select( store ) ).__experimentalHasContentRoleAttribute() + unlock( select( store ) ).privateHasContentRoleAttribute() // Note the unlock() is required. This line wouldn't work: - // select( store ).__experimentalHasContentRoleAttribute() + // select( store ).privateHasContentRoleAttribute() ) ); // Use the private action: - unlock( useDispatch( store ) ).__experimentalToggleFeature(); + unlock( useDispatch( store ) ).privateToggleFeature(); // ... } ``` -##### Experimental functions, classes, and variables +##### Private functions, classes, and variables ```js // In packages/package1/index.js: import { lock } from './private-apis'; -export const experiments = {}; +export const privateApis = {}; /* Attach private data to the exported object */ -lock( experiments, { - __experimentalCallback: function () {}, - __experimentalReactComponent: function ExperimentalComponent() { +lock( privateApis, { + privateCallback: function () {}, + privateReactComponent: function PrivateComponent() { return
; }, - __experimentalClass: class Experiment {}, - __experimentalVariable: 5, + privateClass: class PrivateClass {}, + privateVariable: 5, } ); // In packages/package2/index.js: -import { experiments } from '@wordpress/package1'; +import { privateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; const { - __experimentalCallback, - __experimentalReactComponent, - __experimentalClass, - __experimentalVariable, -} = unlock( experiments ); + privateCallback, + privateReactComponent, + privateClass, + privateVariable, +} = unlock( privateApis ); ``` Remember to always register the private actions and selectors on the **registered** store. @@ -392,10 +319,10 @@ unlock( registeredStore ).registerPrivateActions( { } ); ``` -#### Experimental function arguments +#### Private function arguments -To add an experimental argument to a stable function you'll need -to prepare a stable and an experimental version of that function. +To add a private argument to a stable function you'll need +to prepare a stable and a private version of that function. Then, export the stable function and `lock()` the unstable function inside it: @@ -404,10 +331,10 @@ inside it: import { lock } from './private-apis'; // The experimental function contains all the logic -function __experimentalValidateBlocks( formula, __experimentalIsStrict ) { +function privateValidateBlocks( formula, privateIsStrict ) { let isValid = false; // ...complex logic we don't want to duplicate... - if ( __experimentalIsStrict ) { + if ( privateIsStrict ) { // ... } // ...complex logic we don't want to duplicate... @@ -418,23 +345,25 @@ function __experimentalValidateBlocks( formula, __experimentalIsStrict ) { // The stable public function is a thin wrapper that calls the // experimental function with the experimental features disabled export function validateBlocks( blocks ) { - __experimentalValidateBlocks( blocks, false ); + privateValidateBlocks( blocks, false ); } -lock( validateBlocks, __experimentalValidateBlocks ); + +export const privateApis = {}; +lock( privateApis, { privateValidateBlocks } ); // In @wordpress/package2/index.js: -import { validateBlocks } from '@wordpress/package1'; +import { privateApis as package1PrivateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; // The experimental function may be "unlocked" given the stable function: -const __experimentalValidateBlocks = unlock( validateBlocks ); -__experimentalValidateBlocks( blocks, true ); +const { privateValidateBlocks } = unlock( package1PrivateApis ); +privateValidateBlocks( blocks, true ); ``` -#### Experimental React Component properties +#### Private React Component properties -To add an experimental argument to a stable component you'll need -to prepare a stable and an experimental version of that component. +To add an private argument to a stable component you'll need +to prepare a stable and an private version of that component. Then, export the stable function and `lock()` the unstable function inside it: @@ -442,13 +371,13 @@ inside it: // In @wordpress/package1/index.js: import { lock } from './private-apis'; -// The experimental component contains all the logic -const ExperimentalMyButton = ( { title, __experimentalShowIcon = true } ) => { +// The private component contains all the logic +const PrivateMyButton = ( { title, privateShowIcon = true } ) => { // ...complex logic we don't want to duplicate... return ( ); } @@ -456,27 +385,28 @@ const ExperimentalMyButton = ( { title, __experimentalShowIcon = true } ) => { // The stable public component is a thin wrapper that calls the // experimental component with the experimental features disabled export const MyButton = ( { title } ) => - + -lock(MyButton, ExperimentalMyButton); +export const privateApis = {}; +lock( privateApis, { PrivateMyButton } ); // In @wordpress/package2/index.js: -import { MyButton } from '@wordpress/package1'; +import { privateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; -// The experimental component may be "unlocked" given the stable component: -const ExperimentalMyButton = unlock(MyButton); +// The private component may be "unlocked" given the stable component: +const { PrivateMyButton } = unlock(privateApis); export function MyComponent() { return ( - + ) } ``` -#### Experimental editor settings +#### Private editor settings -WordPress extenders cannot update the experimental block settings on their own. The `updateSettings()` actions of the `@wordpress/block-editor` store will filter out all the settings that are **not** a part of the public API. The only way to actually store them is via private action. `__experimentalUpdateSettings()`. +WordPress extenders cannot update the private block settings on their own. The `updateSettings()` actions of the `@wordpress/block-editor` store will filter out all the settings that are **not** a part of the public API. The only way to actually store them is via the private action `__experimentalUpdateSettings()`. To privatize a block editor setting, add it to the `privateSettings` list in [/packages/block-editor/src/store/actions.js](/packages/block-editor/src/store/actions.js): @@ -487,13 +417,26 @@ const privateSettings = [ ]; ``` -#### Experimental block.json and theme.json APIs +#### Private block.json and theme.json APIs As of today, there is no way to restrict the `block.json` and `theme.json` APIs -to the Gutenberg codebase. In the future, however, the new `__experimental` APIs +to the Gutenberg codebase. In the future, however, the new private APIs will only apply to the core WordPress blocks and plugins and themes will not be able to access them. +#### Inline small actions in thunks + +Finally, instead of introducing a new action creator, consider using a [thunk](/docs/how-to-guides/thunks.md): + +```js +export function toggleFeature( scope, featureName ) { + return function ( { dispatch } ) { + dispatch( { type: '__private_BEFORE_TOGGLE' } ); + // ... + }; +} +``` + ### Objects When possible, use [shorthand notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#New_notations_in_ECMAScript_2015) when defining object property values: From 36eb35aab672c529d2064778d1804bcba0baf28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 10 Feb 2023 17:04:19 +0100 Subject: [PATCH 2/5] Add a section about public experimental APIs --- docs/contributors/code/coding-guidelines.md | 27 ++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index c0a4823b3549b3..575991a46e7b36 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -114,7 +114,7 @@ Example: import VisualEditor from '../visual-editor'; ``` -### Experimental APIs +### Experimental and Private APIs Experimental APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. @@ -437,6 +437,31 @@ export function toggleFeature( scope, featureName ) { } ``` +### Exposing private APIs publicly + +Some private APIs could benefit from community feedback and it makes sense to expose them to WordPress extenders. At the same time, it doesn't make sense to turn them into a public API in WordPress core. What should you do? + +You can re-export that private API as experimental and restrict it to the Gutenberg plugin: + +```js +// This function can't be used by extenders in any context: +function privateApi() {} + +// This function can be used by extenders with the Gutenberg plugin but not in vanilla WordPress Core: +function experimentalApi() {} + +// Gutenberg treats both functions as private APIs internally: +const privateApis = {}; +lock(privateApis, { privateApi, experimentalApi }); + +// The experimental API is explicitly exported but will not be +// merged into WordPress core thanks to the IS_GUTENBERG_PLUGIN +// check. +if ( IS_GUTENBERG_PLUGIN ) { + export const __experimentalApi = unlock( experiments ).experimentalApi; +} +``` + ### Objects When possible, use [shorthand notation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#New_notations_in_ECMAScript_2015) when defining object property values: From d7becb40b9ab0bf649cbee827c75b0bea65fd637 Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Thu, 6 Apr 2023 15:51:58 +0200 Subject: [PATCH 3/5] Explain the difference between experimental APIs and private APIs --- docs/contributors/code/coding-guidelines.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 575991a46e7b36..e5546136773d76 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -116,7 +116,10 @@ import VisualEditor from '../visual-editor'; ### Experimental and Private APIs -Experimental APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. +There are two important API types in the Gutenberg codebase: + +* **Experimental APIs** – they are public APIs that are only shipped in Gutenberg and not merged into WordPress core. Their existence is either pending future revision or provides an immediate means to an end. +* **Private APIs** – they are used internally in the Gutenberg Plugin, are shipped in WordPress Core, but they cannot be accessed by WordPress extenders. Private APIs can be freely changed and removed without affecting existing WordPress websites. _To External Consumers:_ @@ -126,6 +129,8 @@ _To Project Contributors:_ An experimental API is named as such to communicate instability of a function whose interface is not yet finalized. Aside from references within the code, these APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. +#### Experimental APIs are useful for incubating new features before releasing them + An experimental function or object should be: * Prefixed respectively using `__experimental` From 682d8590437ea5e9edb984224b3ec6f41a1ff3eb Mon Sep 17 00:00:00 2001 From: Adam Zielinski Date: Thu, 6 Apr 2023 15:54:03 +0200 Subject: [PATCH 4/5] Update coding-guidelines.md --- docs/contributors/code/coding-guidelines.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index e5546136773d76..e734835f52e1a8 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -121,6 +121,8 @@ There are two important API types in the Gutenberg codebase: * **Experimental APIs** – they are public APIs that are only shipped in Gutenberg and not merged into WordPress core. Their existence is either pending future revision or provides an immediate means to an end. * **Private APIs** – they are used internally in the Gutenberg Plugin, are shipped in WordPress Core, but they cannot be accessed by WordPress extenders. Private APIs can be freely changed and removed without affecting existing WordPress websites. +#### Experimental APIs are useful for incubating new features before releasing them + _To External Consumers:_ **There is no support commitment for experimental APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. @@ -129,8 +131,6 @@ _To Project Contributors:_ An experimental API is named as such to communicate instability of a function whose interface is not yet finalized. Aside from references within the code, these APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. -#### Experimental APIs are useful for incubating new features before releasing them - An experimental function or object should be: * Prefixed respectively using `__experimental` From 5b3e255ec1e6397645d84d994b4226514131e595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 12 Apr 2023 15:11:13 +0200 Subject: [PATCH 5/5] Do not formalize plugin-only __experimental APIs. Talk about plugin-only APIs instead --- docs/contributors/code/coding-guidelines.md | 85 ++++++++------------- 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index e734835f52e1a8..0e8ae651a0c09e 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -114,64 +114,45 @@ Example: import VisualEditor from '../visual-editor'; ``` -### Experimental and Private APIs +### Legacy Experimental APIs, Plugin-only APIs, and Private APIs -There are two important API types in the Gutenberg codebase: +#### Legacy Experimental APIs -* **Experimental APIs** – they are public APIs that are only shipped in Gutenberg and not merged into WordPress core. Their existence is either pending future revision or provides an immediate means to an end. -* **Private APIs** – they are used internally in the Gutenberg Plugin, are shipped in WordPress Core, but they cannot be accessed by WordPress extenders. Private APIs can be freely changed and removed without affecting existing WordPress websites. +Historically, Gutenberg has used the `__experimental` and `__unstable` prefixes to indicate that a given API is not yet stable and may be subject to change. This is a legacy convention which should be avoided in favor of the plugin-only API pattern or a private API pattern described below. -#### Experimental APIs are useful for incubating new features before releasing them +The problem with using the prefixes was that these APIs rarely got stabilized or removed. As of June 2022, WordPress Core contained 280 publicly exported experimental APIs merged from the Gutenberg plugin during the major WordPress releases. Many plugins and themes started relying on these experimental APIs for essential features that couldn't be accessed in any other way. -_To External Consumers:_ +The legacy `__experimental` APIs can't be removed on a whim anymore. They became a part of the WordPress public API and fall under the [WordPress Backwards Compatibility policy](https://developer.wordpress.org/block-editor/contributors/code/backward-compatibility/). Removing them involves a deprecation process. It may be relatively easy for some APIs, but it may require effort and span multiple WordPress releases for others. -**There is no support commitment for experimental APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. +All in all, don't use the `__experimental` prefix for new APIs. Use plugin-only APIs and private APIs instead. -_To Project Contributors:_ +#### Plugin-only APIs -An experimental API is named as such to communicate instability of a function whose interface is not yet finalized. Aside from references within the code, these APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. +Plugin-only APIs are temporary values exported from a module whose existence is either pending future revision or provides an immediate means to an end. -An experimental function or object should be: +_To External Consumers:_ -* Prefixed respectively using `__experimental` -* Only exported in the Gutenberg plugin so it never becomes a part of WordPress core +**There is no support commitment for plugin-only APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs. -Here's an example: +_To Project Contributors:_ + +An **plugin-only API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. It should be made stable or removed at the earliest opportunity. + +Plugin-only APIs are excluded from WordPress Core and only available in the Gutenberg Plugin: ```js -// Using IS_GUTENBERG_PLUGIN allows Webpack to exclude the -// experimental exports from WordPress core: +// Using IS_GUTENBERG_PLUGIN allows Webpack to exclude this +// export from WordPress core: if ( IS_GUTENBERG_PLUGIN ) { - export { __experimentalDoExcitingExperimentalAction } from './api'; + export { doSomethingExciting } from './api'; } ``` -An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion. It should be made stable or removed at the earliest opportunity. - -While an experimental API may often stabilize into a publicly-available API, there is no guarantee that it will. The conversion to a stable API will inherently be considered a breaking change by the mere fact that the function name must be changed to remove the `__experimental` prefix. - -Other prefixes were also used in the past, such as `__unstable` or `__internal`. - -#### Experimental APIs merged into WordPress Core become a liability - -**Avoid introducing public experimental APIs.** - -As of June 2022, WordPress Core contains 280 publicly exported experimental APIs. They got merged from the Gutenberg -plugin during the major WordPress releases. Many plugins and themes rely on these experimental APIs for essential -features that can't be accessed in any other way. Naturally, these APIs can't be removed without a warning anymore. -They are a part of the WordPress public API and fall under the -[WordPress Backwards Compatibility policy](https://developer.wordpress.org/block-editor/contributors/code/backward-compatibility/). -Removing them involves a deprecation process. It may be relatively easy for some APIs, but it may require effort and -span multiple WordPress releases for others. - -**Use private APIs instead.** - -Make your experimental APIs private and don't expose them to WordPress extenders. +The public interface of such APIs is not yet finalized. Aside from references within the code, they APIs should neither be documented nor mentioned in any CHANGELOG. They should effectively be considered to not exist from an external perspective. In most cases, they should only be exposed to satisfy requirements between packages maintained in this repository. -This way they'll remain internal implementation details that can be changed or removed -without a warning and without breaking WordPress plugins. +While a plugin-only API may often stabilize into a publicly-available API, there is no guarantee that it will. -#### Private exports are supported – use the `lock()` and `unlock()` API from `@wordpress/private-apis` +#### Private APIs Each `@wordpress` package wanting to privately access or expose a private APIs can do so by opting-in to `@wordpress/private-apis`: @@ -335,7 +316,7 @@ inside it: // In @wordpress/package1/index.js: import { lock } from './private-apis'; -// The experimental function contains all the logic +// A private function contains all the logic function privateValidateBlocks( formula, privateIsStrict ) { let isValid = false; // ...complex logic we don't want to duplicate... @@ -348,7 +329,7 @@ function privateValidateBlocks( formula, privateIsStrict ) { } // The stable public function is a thin wrapper that calls the -// experimental function with the experimental features disabled +// private function with the private features disabled export function validateBlocks( blocks ) { privateValidateBlocks( blocks, false ); } @@ -360,7 +341,7 @@ lock( privateApis, { privateValidateBlocks } ); import { privateApis as package1PrivateApis } from '@wordpress/package1'; import { unlock } from './private-apis'; -// The experimental function may be "unlocked" given the stable function: +// The private function may be "unlocked" given the stable function: const { privateValidateBlocks } = unlock( package1PrivateApis ); privateValidateBlocks( blocks, true ); ``` @@ -388,7 +369,7 @@ const PrivateMyButton = ( { title, privateShowIcon = true } ) => { } // The stable public component is a thin wrapper that calls the -// experimental component with the experimental features disabled +// private component with the private features disabled export const MyButton = ( { title } ) => @@ -446,24 +427,24 @@ export function toggleFeature( scope, featureName ) { Some private APIs could benefit from community feedback and it makes sense to expose them to WordPress extenders. At the same time, it doesn't make sense to turn them into a public API in WordPress core. What should you do? -You can re-export that private API as experimental and restrict it to the Gutenberg plugin: +You can re-export that private API as a plugin-only API to expose it publicly only in the Gutenberg plugin: ```js // This function can't be used by extenders in any context: -function privateApi() {} +function privateEverywhere() {} // This function can be used by extenders with the Gutenberg plugin but not in vanilla WordPress Core: -function experimentalApi() {} +function privateInCorePublicInPlugin() {} // Gutenberg treats both functions as private APIs internally: const privateApis = {}; -lock(privateApis, { privateApi, experimentalApi }); +lock(privateApis, { privateEverywhere, privateInCorePublicInPlugin }); -// The experimental API is explicitly exported but will not be -// merged into WordPress core thanks to the IS_GUTENBERG_PLUGIN -// check. +// The privateInCorePublicInPlugin function is explicitly exported, +// but this export will not be merged into WordPress core thanks to +// the IS_GUTENBERG_PLUGIN check. if ( IS_GUTENBERG_PLUGIN ) { - export const __experimentalApi = unlock( experiments ).experimentalApi; + export const privateInCorePublicInPlugin = unlock( privateApis ).privateInCorePublicInPlugin; } ```