Skip to content

Commit

Permalink
Limit access to experimental APIs to WordPress codebase (#43386)
Browse files Browse the repository at this point in the history
Make the __experimental APIs private by introducing a dealer mechanism that only grants access to core WordPress packages.

It solves the problem of leaking private experimental APIs to extenders in public stable releases. See #40316 for more details.

Usage example:

```js
// 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: () => { /* ... */ },
});
```

```js
// 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();
```
  • Loading branch information
adamziel committed Sep 26, 2022
1 parent 3358251 commit ace58e4
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1607,6 +1607,12 @@
"markdown_source": "../packages/eslint-plugin/README.md",
"parent": "packages"
},
{
"title": "@wordpress/experiments",
"slug": "packages-experiments",
"markdown_source": "../packages/experiments/README.md",
"parent": "packages"
},
{
"title": "@wordpress/format-library",
"slug": "packages-format-library",
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@wordpress/editor": "file:packages/editor",
"@wordpress/element": "file:packages/element",
"@wordpress/escape-html": "file:packages/escape-html",
"@wordpress/experiments": "file:packages/experiments",
"@wordpress/format-library": "file:packages/format-library",
"@wordpress/hooks": "file:packages/hooks",
"@wordpress/html-entities": "file:packages/html-entities",
Expand Down
35 changes: 35 additions & 0 deletions packages/experiments/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@wordpress/dependency-injection",
"version": "0.0.1",
"description": "Dependency Injection container for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"gutenberg",
"dom",
"utils"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/dependency-injection/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git",
"directory": "packages/injection"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"engines": {
"node": ">=12"
},
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"sideEffects": false,
"dependencies": {
"@babel/runtime": "^7.16.0"
},
"publishConfig": {
"access": "public"
}
}
85 changes: 85 additions & 0 deletions packages/experiments/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const CORE_MODULES_USING_EXPERIMENTS = [
'@wordpress/data',
'@wordpress/block-editor',
'@wordpress/block-library',
'@wordpress/blocks',
'@wordpress/core-data',
'@wordpress/date',
'@wordpress/edit-site',
'@wordpress/edit-widgets',
];

const registeredExperiments = {};
/*
* Warning for theme and plugin developers.
*
* The use of experimental developer APIs is intended for use by WordPress Core
* and the Gutenberg plugin exclusively.
*
* Dangerously opting in to using these APIs is NOT RECOMMENDED. Furthermore,
* the WordPress Core philosophy to strive to maintain backward compatibility
* for third-party developers DOES NOT APPLY to experimental APIs.
*
* THE CONSENT STRING FOR OPTING IN TO THESE APIS MAY CHANGE AT ANY TIME AND
* WITHOUT NOTICE. THIS CHANGE WILL BREAK EXISTING THIRD-PARTY CODE. SUCH A
* CHANGE MAY OCCUR IN EITHER A MAJOR OR MINOR RELEASE.
*/
const requiredConsent =
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.';

export const __dangerousOptInToUnstableAPIsOnlyForCoreModules = (
consent,
moduleName
) => {
if ( ! CORE_MODULES_USING_EXPERIMENTS.includes( moduleName ) ) {
throw new Error(
`You tried to opt-in to unstable APIs as a module "${ moduleName }". ` +
'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 be removed ' +
'without a warning. If you ignore this error and depend on unstable features, ' +
'your product will inevitably break on one of the next WordPress releases.'
);
}
if ( moduleName in registeredExperiments ) {
throw new Error(
`You tried to opt-in to unstable APIs as a module "${ moduleName }" which is already registered. ` +
'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 be removed ' +
'without a warning. If you ignore this error and depend on unstable features, ' +
'your product will inevitably break on one of the next WordPress releases.'
);
}
if ( consent !== requiredConsent ) {
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 ' +
'without a warning. If you ignore this error and depend on unstable features, ' +
'your product will inevitably break on the next WordPress release.'
);
}
registeredExperiments[ moduleName ] = {
accessKey: {},
apis: {},
};
return {
register: ( experiments ) => {
for ( const key in experiments ) {
registeredExperiments[ moduleName ].apis[ key ] =
experiments[ key ];
}
return registeredExperiments[ moduleName ].accessKey;
},
unlock: ( accessKey ) => {
for ( const experiment of Object.values( registeredExperiments ) ) {
if ( experiment.accessKey === accessKey ) {
return experiment.apis;
}
}

throw new Error(
'There is no registered module matching the specified access key'
);
},
};
};
84 changes: 84 additions & 0 deletions packages/experiments/src/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Internal dependencies
*/
import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '../';

const requiredConsent =
'I know using unstable features means my plugin or theme will inevitably break on the next WordPress release.';

describe( '__dangerousOptInToUnstableAPIsOnlyForCoreModules', () => {
it( 'Should require a consent string', () => {
expect( () => {
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
'',
'@wordpress/data'
);
} ).toThrow( /without confirming you know the consequences/ );
} );
it( 'Should require a valid @wordpress package name', () => {
expect( () => {
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
requiredConsent,
'custom_package'
);
} ).toThrow(
/This feature is only for JavaScript modules shipped with WordPress core/
);
} );
it( 'Should not register the same module twice', () => {
expect( () => {
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
requiredConsent,
'@wordpress/edit-widgets'
);
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
requiredConsent,
'@wordpress/edit-widgets'
);
} ).toThrow( /is already registered/ );
} );
it( 'Should grant access to unstable APIs when passed both a consent string and a previously unregistered package name', () => {
const unstableAPIs = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
requiredConsent,
'@wordpress/edit-site'
);
expect( unstableAPIs.unlock ).toEqual( expect.any( Function ) );
expect( unstableAPIs.register ).toEqual( expect.any( Function ) );
} );
it( 'Should register and unlock experimental APIs', () => {
// This would live in @wordpress/data:
// Opt-in to experimental APIs
const dataExperiments =
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
requiredConsent,
'@wordpress/data'
);

// Register the experimental APIs
const dataExperimentalFunctions = {
__experimentalFunction: jest.fn(),
};
const dataAccessKey = dataExperiments.register(
dataExperimentalFunctions
);

// This would live in @wordpress/core-data:
// Register the experimental APIs
const coreDataExperiments =
__dangerousOptInToUnstableAPIsOnlyForCoreModules(
requiredConsent,
'@wordpress/core-data'
);

// Get the experimental APIs registered by @wordpress/data
const { __experimentalFunction } =
coreDataExperiments.unlock( dataAccessKey );

// Call one!
__experimentalFunction();

expect(
dataExperimentalFunctions.__experimentalFunction
).toHaveBeenCalled();
} );
} );

0 comments on commit ace58e4

Please sign in to comment.