diff --git a/docs/manifest.json b/docs/manifest.json index 69d991c65d3da5..b3cabe1e3a7e8c 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 9a6b5e36f7372e..6322e01c8590c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17438,6 +17438,12 @@ "requireindex": "^1.2.0" } }, + "@wordpress/experiments": { + "version": "file:packages/experiments", + "requires": { + "@babel/runtime": "^7.16.0" + } + }, "@wordpress/format-library": { "version": "file:packages/format-library", "requires": { diff --git a/package.json b/package.json index 07ab29258c7a66..0f2900f097e976 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/experiments/package.json b/packages/experiments/package.json new file mode 100644 index 00000000000000..85310d82565324 --- /dev/null +++ b/packages/experiments/package.json @@ -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" + } +} diff --git a/packages/experiments/src/index.js b/packages/experiments/src/index.js new file mode 100644 index 00000000000000..033816e4b28365 --- /dev/null +++ b/packages/experiments/src/index.js @@ -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' + ); + }, + }; +}; diff --git a/packages/experiments/src/test/index.js b/packages/experiments/src/test/index.js new file mode 100644 index 00000000000000..a3424ac26f7e20 --- /dev/null +++ b/packages/experiments/src/test/index.js @@ -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(); + } ); +} );