-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Limit access to experimental APIs to WordPress codebase with a new experiments package #43386
Conversation
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.'; | ||
|
||
export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( | ||
consent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to ensure calls to this function use a string (as opposed to a variable set to a string).
// Allowed
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
...
);
// Not allowed
const yeahWhatever = 'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.';
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
yeahWhatever,
...
);
I suspect the answer is no but would rather check :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately no :-(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although this is be possible:
import {
__dangerousOptInToUnstableAPIsOnlyForCoreModules,
_i_know_using_unstable_features_means_my_plugin_or_theme_will_inevitably_break_on_the_next_WordPress_release
} from '@wordpress/experiments';
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
_i_know_using_unstable_features_means_my_plugin_or_theme_will_inevitably_break_on_the_next_WordPress_release,
...
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loving the huge, awkward consent message 😂
Going the importable variable route would definitely make things easier. I don't know if we want to make things easier? But either way, we'll end up copying and pasting the message in, so the added difficulty is only in locating a place to copy from.
throw new Error( | ||
`You tried to opt-in to unstable APIs without confirming you know the consequences. ` + | ||
'This feature is only for JavaScript modules shipped with WordPress core. ' + | ||
'Please do not use it in plugins and themes as the unstable APIs will removed ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this message a string and reuse it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this solution to be pretty good, a wrapper package acting as a cloak around experiments and practically hiding them from a generic global access point.
I think the opt in can be less verbose - we're a bit over doing it, but the technical approach looks good enough to me. TBH this is more than enough for what is a best practice anyway.
Agreed, however it is perfect to show how far we can go to highlight that experiments are meant to be for internal usage only. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a solution that fits our needs, thank you for working on it @adamziel!
There are some cases where using this solution is not so direct:
- Experimental properties in a component.
- Experimental keys in a "settings" object.
- Experimental parameters in a function.
- Experimental block registration settings in block.json (e.g:__experimentalSelector, __experimentalRole)
I guess for some cases like the experimental properties in a component, we can expose two components, a stable and an Experimental one, where Experimental properties are only supported on the Experimental component. The same for experimental parameters in a function. It may be tricky because sometimes these experimental properties are passed down multiple levels, so we need to create many experimental components/functions just because it is passing down a property, but I think it is acceptable.
For things like experimental block.json properties, the scenario is more complex. Maybe we can have an experimental function that needs to be called when using experimental block.json properties or something similar.
Thank you for reviewing @jorgefilipecosta! As you say, there are some other tricky cases to handle, but this at least gets us moving.
I think you're on to something here!
The publicly exported function would have no const __experimentalDoGreatThings = ( id, __experimentalArgument ) => {
if( __experimentalArgument ) {
// ... do something ...
}
}
export const doGreatThings = ( id ) => __experimentalDoGreatThings( id, false );
registerExperimentalAPI({
__experimentalDoGreatThings
});
Most components are just functions so your idea still applies!
Core components can render the experimental versions by default without adding too much complexity: const __experimentalRoot = ({ id, __experimentalArgument }) => {
return <__experimentalBranch id={id} __experimentalArgument={__experimentalArgument} />
}
const __experimentalBranch = ({ id, __experimentalArgument }) => {
return <__experimentalLeaf id={id} __experimentalArgument={__experimentalArgument} />
}
const __experimentalLeaf = ({ id, __experimentalArgument }) => {
// ...
}
export const Root = ({ id }) => __experimentalRoot({ id });
export const Branch = ({ id }) => __experimentalBranch({ id });
export const Leaf = ({ id }) => __experimentalLeaf({ id });
registerExperimentalAPI({
__experimentalRoot,
__experimentalBranch,
__experimentalLeaf
});
I can't think of any way around that, although we could make it harder and don't accept these in PHP setup functions.
Since WordPress has a baked-in list of core blocks, perhaps the idea below would work? function parseBlockJson( $string ) {
$data = json_decode( $string );
if( ! is_core_block( $data['name'] ) ) {
$data = remove_experimental_properties( $data );
}
// ...
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for looking into this, looks like a solid start to addressing the issue!
I guess for some cases like the experimental properties in a component, we can expose two components, a stable and an Experimental one, where Experimental properties are only supported on the Experimental component. The same for experimental parameters in a function. It may be tricky because sometimes these experimental properties are passed down multiple levels, so we need to create many experimental components/functions just because it is passing down a property, but I think it is acceptable.
Yeah, this is an interesting problem. I wonder if the added complexity to creating experimental parameters might result in us doing so less often. There's no rule about changes of that sort having to be marked experimental. Or should we try to enforce anything along those lines too?
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.'; | ||
|
||
export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = ( | ||
consent, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Loving the huge, awkward consent message 😂
Going the importable variable route would definitely make things easier. I don't know if we want to make things easier? But either way, we'll end up copying and pasting the message in, so the added difficulty is only in locating a place to copy from.
@tellthemachines that's good IMHO, you can't use this without inspecting the code and realizing why you are not supposed to do that. |
@peterwilsoncc @azaozz @Clorith @tradesouthwest can you spot any problems with the approach proposed here? If not, let's move forward with this. |
My understanding is that this proposal addresses all concerns discussed in the earlier proposal #41278. It allows to expose code between scripts without exposing it in the public API. The added benefit is that we will have a single place to update if we want to collect details on the number and longevity of the experiments run. To the last point, I would be happy to see the required Gutenberg and/or version number when the API was introduced. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am so up for this. I've added a suggestion for a fairly unambiguous comment. I'm not sure if it's best as a comment or as a part of the docblock for the dangerous export function.
It's not very subtle :)
Great feedback! We definitely need to take that into account so we can enforce that the API is already registered 👍🏻 Would it work with a simpler API like this? import { __experimentalUnlock } from '@wordpress/data';
const { doExperimentalDataStuff } = __experimentalUnlock( secretTokenOrSomethingElseFromTheCurrentPackage ); |
I don't understand this example. Should it look more like this? import { __experimentalUnlock } from '@wordpress/experiments';
import { secretTokenOrSomethingElseFromTheCurrentPackage } from '@wordpress/data';
const { doExperimentalDataStuff } = __experimentalUnlock( secretTokenOrSomethingElseFromTheCurrentPackage ); |
056742a
to
3cc920f
Compare
@jsnajdr I went for the following API – what do you think? // in @wordpress/data
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments';
const experiments = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
'@wordpress/data'
);
export const __experiments = experiments.register({
__experimentalFunction: () => { /* ... */ },
}); // In @wordpress/core-data:
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/experiments';
import { __experiments as __dataExperiments } from '@wordpress/data';
const experiments = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.',
'@wordpress/core-data'
);
// Get the experimental APIs registered by @wordpress/data
const { __experimentalFunction } = experiments.unlock( __dataExperiments );
__experimentalFunction();
I like it! |
@adamziel, so you always return only access keys ( |
Thanks @gziolo! I went ahead and merged. Since this code is internal by definition, we can always adjust it in a follow-up PR. |
@@ -0,0 +1,35 @@ | |||
{ | |||
"name": "@wordpress/dependency-injection", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have just noticed that it should be @wordpress/experiments
before we start using it.
As a follow-up, we need to add |
Thanks for seeing this through Adam. I haven't had much chance to offer feedback, but it's encouraging to see some solutions coming together. It'd be great to also add or update contributor docs in addition to the readme: Even if it's just a brief summary with a link to the package's readme. |
I only got to this now, let me share some observations: The const { unlock } = __dangerouslyOptIn( 'I know', '@wordpress/data' ); but then can use it to unlock any other module, too: import { __blockEditorExperiments } from '@wordpress/block-editor';
const { doBlockEditorStuff } = unlock( __blockEditorExperiments ); Is that desired? I would say it isn't, because I dangerously opted in and consented only to the If omniponent return {
register: ( experiments ) => { ... },
unlock: ( key ) => { ... },
} but can have one global shared one function unlock( key ) {
...
}
return {
register: ( experiments ) => { ... },
unlock,
} That's just a little clarification/optimization, nothing else. In #44521 I also see a little confusion about naming the "access key". Sometimes it's like I like the latter, when we treat it as the object itself, and are just unlocking it. |
Introduces a private selectors APIs in `@wordpress/data` via [the new `@wordpress/experimental`](#43386 (comment)) package: ```js // Package wordpress/block-data: import { unlock } from '../experiments'; import { experiments as dataExperiments } from '@wordpress/data'; const { registerPrivateActionsAndSelectors } = unlock( dataExperiments ); import { store as blockEditorStore } from './store'; import { __unstableSelectionHasUnmergeableBlock } from './store/selectors'; registerPrivateActionsAndSelectors( store, {}, { __experimentalHasContentRoleAttribute } ); // plain usage unlock( registry.select( blockEditorStore ) ).getContentLockingParent(); // usage in React useSelect( ( select ) => ( { parent: privateOf( select( blockEditorStore ) ).__unstableSelectionHasUnmergeableBlock(); } ) ); ```
Added Needs Dev Note to get it into the Fieldguide for 6.2 |
It's fully documented in this place: It doesn't need to be necessarily treated as a dev note, but it surely deserves an update on the Make Core blog. It might also be a good idea to consider changing the sentiment for legacy experimental and unstable APIs, as I don't think we want them to use them anymore now that we have a proper API to achieve the same goal. |
What
This PR proposes a way of making the
__experimental
APIs private as exposing them to publicly plugin authors is quite controversial..Tl;dr, the idea is to have a "dealer" that only deals the experimental APIs to WordPress packages.
Each package would start by registering itself:
The function name communicates that plugins are not supposed to use it. To make double and triple sure, the first argument must be that exact consent string, and the second argument must be a known
@wordpress
package that hasn't opted in yet – otherwise an error is thrown.Exposing a new
__experimental
API looks like this:Now here's the interesting part – consuming a registered
__experimental
API requires opting-in first:window.wp
is still the recommended way of using the public APIs. A determined developer who would want to use the experimental APIs at all costs would have to:@wordpress
package (and trigger an error as soon as the real package is loaded)This PR touches 21 files but the important part is the new
@wordpress/experiments
module. Everything else is just a demo that I want to remove before merging this PR. There is no need to migrate the existing__experimental
APIs to this new system as they are already being publicly exported. Only the future__experimental
APIs would use this system.Notable usage examples
The usage examples previously shipped with this PR include:
@wordpress/data
offers experimental utils for private actions and selectors__experimentalReapplyBlockTypeFilters()
is one such private action – and here's where it's used.@wordpress/edit-widgets
uses experiments registered by three different packages@wordpress/data
only registers experimental APIs without requesting anySee the entire (now removed) commit tree.
This PR is an alternative to #41278
Testing Instructions
Ignore the CI. Let's focus on the discussion – what do you like and dislike about this idea?
CC @gziolo @peterwilsoncc @mcsf @talldan @noisysocks @azaozz @jorgefilipecosta @mtias