From 5a7704922d8f9f739529f738c93e25c55b0d26e9 Mon Sep 17 00:00:00 2001 From: Willis Allstead Date: Mon, 23 Nov 2020 02:28:27 -0800 Subject: [PATCH 001/317] Replace 'Remove from Reusable blocks' with 'Manage Reusable blocks'. (#27026) * Replace 'Remove from Reusable blocks' with 'Manage Reusable blocks'. * Removed ReusableBlockDeleteButton component and added ReusableBlocksManageButton in its place. * On click, ReusableBlocksManageButton sends the user to the reusable blocks management screen. * Added '@wordpress/url' dependency to the package. This is used to build the url for the reusable blocks management screen. Fixes #12791. * Fix logic and dependency for new 'Manage Reusable blocks' menu item. * Use file link for new @wordpress/url dependency. * Rename abbreviated blockObj variable to reusableBlock to follow coding standards. * Simplified logic to check if the 'Manage Reusable blocks' menu item should be visible. * Use MenuItem's href prop to create an a tag instead of relying on onClick on a button. Fixes #27026. * Update package lock. (#27026) * Remove unused e2e test for deleting reusable blocks. (#27026) --- package-lock.json | 1 + .../editor/various/reusable-blocks.test.js | 36 ------- packages/reusable-blocks/package.json | 1 + .../reusable-blocks-menu-items/index.js | 4 +- .../reusable-block-delete-button.js | 98 ------------------- .../reusable-blocks-manage-button.js | 47 +++++++++ 6 files changed, 51 insertions(+), 136 deletions(-) delete mode 100644 packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-delete-button.js create mode 100644 packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js diff --git a/package-lock.json b/package-lock.json index 78edc4dff6a2e..83e461e91fda3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18232,6 +18232,7 @@ "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", "@wordpress/notices": "file:packages/notices", + "@wordpress/url": "file:packages/url", "lodash": "^4.17.19" } }, diff --git a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js index 3cd130ec754ff..0d7c6d8baf236 100644 --- a/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/reusable-blocks.test.js @@ -8,19 +8,12 @@ import { createNewPost, clickBlockToolbarButton, pressKeyWithModifier, - searchForReusableBlock, getEditedPostContent, trashAllPosts, visitAdminPage, toggleGlobalBlockInserter, } from '@wordpress/e2e-test-utils'; -function waitForAndAcceptDialog() { - return new Promise( ( resolve ) => { - page.once( 'dialog', () => resolve() ); - } ); -} - describe( 'Reusable blocks', () => { beforeAll( async () => { await createNewPost(); @@ -241,35 +234,6 @@ describe( 'Reusable blocks', () => { expect( text ).toMatch( 'Oh! Hello there!' ); } ); - it( 'can be deleted', async () => { - // Insert the reusable block we edited above - await insertReusableBlock( 'Surprised greeting block' ); - - // Delete the block and accept the confirmation dialog - await clickBlockToolbarButton( 'More options' ); - const deleteButton = await page.waitForXPath( - '//button/span[text()="Remove from Reusable blocks"]' - ); - await Promise.all( [ waitForAndAcceptDialog(), deleteButton.click() ] ); - - // Wait for deletion to finish - await page.waitForXPath( - '//*[contains(@class, "components-snackbar")]/*[text()="Block deleted."]' - ); - - // Check that we have an empty post again - expect( await getEditedPostContent() ).toBe( '' ); - - // Search for the block in the inserter - await searchForReusableBlock( 'Surprised greeting block' ); - - // Check that we couldn't find it - const items = await page.$$( - '.block-editor-block-types-list__item[aria-label="Surprised greeting block"]' - ); - expect( items ).toHaveLength( 0 ); - } ); - it( 'can be created from multiselection', async () => { await createNewPost(); diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index 053f169e56128..a2cde9d2a2ebb 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -35,6 +35,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", "@wordpress/notices": "file:../notices", + "@wordpress/url": "file:../url", "lodash": "^4.17.19" }, "publishConfig": { diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/index.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/index.js index ed68dd7fec771..ce5883402896d 100644 --- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/index.js +++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/index.js @@ -7,7 +7,7 @@ import { withSelect } from '@wordpress/data'; * Internal dependencies */ import ReusableBlockConvertButton from './reusable-block-convert-button'; -import ReusableBlockDeleteButton from './reusable-block-delete-button'; +import ReusableBlocksManageButton from './reusable-blocks-manage-button'; function ReusableBlocksMenuItems( { clientIds, rootClientId } ) { return ( @@ -17,7 +17,7 @@ function ReusableBlocksMenuItems( { clientIds, rootClientId } ) { rootClientId={ rootClientId } /> { clientIds.length === 1 && ( - + ) } ); diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-delete-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-delete-button.js deleted file mode 100644 index ac3abd0bb471c..0000000000000 --- a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-block-delete-button.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * WordPress dependencies - */ -import { MenuItem } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { isReusableBlock } from '@wordpress/blocks'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { useCallback } from '@wordpress/element'; -import { BlockSettingsMenuControls } from '@wordpress/block-editor'; - -/** - * Internal dependencies - */ -import { store } from '../../store'; - -function ReusableBlockDeleteButton( { clientId } ) { - const { isVisible, isDisabled, block } = useSelect( - ( select ) => { - const { getBlock } = select( 'core/block-editor' ); - const { canUser } = select( 'core' ); - const blockObj = getBlock( clientId ); - - const reusableBlock = - blockObj && isReusableBlock( blockObj ) - ? select( 'core' ).getEntityRecord( - 'postType', - 'wp_block', - blockObj.attributes.ref - ) - : null; - - return { - block: blockObj, - isVisible: - !! reusableBlock && - ( reusableBlock.isTemporary || - !! canUser( 'delete', 'blocks', reusableBlock.id ) ), - isDisabled: reusableBlock && reusableBlock.isTemporary, - }; - }, - [ clientId ] - ); - - const { - __experimentalDeleteReusableBlock: deleteReusableBlock, - } = useDispatch( store ); - - const { createSuccessNotice, createErrorNotice } = useDispatch( - 'core/notices' - ); - const onDelete = useCallback( - async function () { - try { - await deleteReusableBlock( block.attributes.ref ); - createSuccessNotice( __( 'Block deleted.' ), { - type: 'snackbar', - } ); - } catch ( error ) { - createErrorNotice( error.message, { - type: 'snackbar', - } ); - } - }, - [ block ] - ); - - if ( ! isVisible ) { - return null; - } - - return ( - - { ( { onClose } ) => ( - { - // eslint-disable-next-line no-alert - const hasConfirmed = window.confirm( - // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace - __( - 'Are you sure you want to delete this Reusable Block?\n\n' + - 'It will be permanently removed from all posts and pages that use it.' - ) - ); - if ( hasConfirmed ) { - onDelete(); - onClose(); - } - } } - > - { __( 'Remove from Reusable blocks' ) } - - ) } - - ); -} - -export default ReusableBlockDeleteButton; diff --git a/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js new file mode 100644 index 0000000000000..ec4df75146bc0 --- /dev/null +++ b/packages/reusable-blocks/src/components/reusable-blocks-menu-items/reusable-blocks-manage-button.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { isReusableBlock } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +import { BlockSettingsMenuControls } from '@wordpress/block-editor'; +import { addQueryArgs } from '@wordpress/url'; + +function ReusableBlocksManageButton( { clientId } ) { + const { isVisible } = useSelect( + ( select ) => { + const { getBlock } = select( 'core/block-editor' ); + const { canUser } = select( 'core' ); + const reusableBlock = getBlock( clientId ); + + return { + isVisible: + !! reusableBlock && + isReusableBlock( reusableBlock ) && + !! canUser( + 'update', + 'blocks', + reusableBlock.attributes.ref + ), + }; + }, + [ clientId ] + ); + + if ( ! isVisible ) { + return null; + } + + return ( + + + { __( 'Manage Reusable blocks' ) } + + + ); +} + +export default ReusableBlocksManageButton; From c59cbd1ebecfd37a648c31f3da5a6f70717decb5 Mon Sep 17 00:00:00 2001 From: Yuliyan Slavchev Date: Mon, 23 Nov 2020 12:55:27 +0200 Subject: [PATCH 002/317] Hooks: Use instance's `doAction` for built-in hooks (#26498) Bug fix: Use instance's own doAction method to trigger built-in hookAdded and hookRemoved hooks. Hooks are currently using doAction from the shared global instance to trigger the built-in hooks hookAdded and hookRemoved. This PR binds the methods for adding and removing hooks to their own hooks instance so that they use doAction from the given instance for the said built-in hooks. --- packages/hooks/CHANGELOG.md | 4 ++ packages/hooks/src/createAddHook.js | 26 ++++++--- packages/hooks/src/createCurrentHook.js | 12 +++- packages/hooks/src/createDidHook.js | 11 ++-- packages/hooks/src/createDoingHook.js | 13 +++-- packages/hooks/src/createHasHook.js | 13 +++-- packages/hooks/src/createHooks.js | 68 +++++++++++++--------- packages/hooks/src/createRemoveHook.js | 29 +++++----- packages/hooks/src/createRunHook.js | 27 +++++---- packages/hooks/src/index.js | 11 +++- packages/hooks/src/test/index.test.js | 76 +++++++++++++++++++++++++ 11 files changed, 210 insertions(+), 80 deletions(-) diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 7e9b150d0f8b2..53c17fccf741c 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -6,6 +6,10 @@ - Include TypeScript type declarations ([#26430](https://github.com/WordPress/gutenberg/pull/26430)) +### Bug Fix + +- Fix: Use own instance's `doAction` method for built-in `hookAdded` and `hookRemoved` hooks ([#26498](https://github.com/WordPress/gutenberg/pull/26498)) + ## 2.6.0 (2019-08-29) ### New Feature diff --git a/packages/hooks/src/createAddHook.js b/packages/hooks/src/createAddHook.js index 9205921ba8546..54af960956d43 100644 --- a/packages/hooks/src/createAddHook.js +++ b/packages/hooks/src/createAddHook.js @@ -3,7 +3,6 @@ */ import validateNamespace from './validateNamespace.js'; import validateHookName from './validateHookName.js'; -import { doAction } from './'; /** * @callback AddHook @@ -13,18 +12,21 @@ import { doAction } from './'; * @param {string} hookName Name of hook to add * @param {string} namespace The unique namespace identifying the callback in the form `vendor/plugin/function`. * @param {import('.').Callback} callback Function to call when the hook is run - * @param {number} [priority=10] Priority of this hook + * @param {number} [priority=10] Priority of this hook */ /** * Returns a function which, when invoked, will add a hook. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey * * @return {AddHook} Function that adds a new hook. */ -function createAddHook( hooks ) { +function createAddHook( hooks, storeKey ) { return function addHook( hookName, namespace, callback, priority = 10 ) { + const hooksStore = hooks[ storeKey ]; + if ( ! validateHookName( hookName ) ) { return; } @@ -50,9 +52,9 @@ function createAddHook( hooks ) { const handler = { callback, priority, namespace }; - if ( hooks[ hookName ] ) { + if ( hooksStore[ hookName ] ) { // Find the correct insert index of the new hook. - const handlers = hooks[ hookName ].handlers; + const handlers = hooksStore[ hookName ].handlers; /** @type {number} */ let i; @@ -74,7 +76,7 @@ function createAddHook( hooks ) { // we're adding would come after the current callback, there's no // problem; otherwise we need to increase the execution index of // any other runs by 1 to account for the added element. - hooks.__current.forEach( ( hookInfo ) => { + hooksStore.__current.forEach( ( hookInfo ) => { if ( hookInfo.name === hookName && hookInfo.currentIndex >= i @@ -84,14 +86,20 @@ function createAddHook( hooks ) { } ); } else { // This is the first hook of its type. - hooks[ hookName ] = { + hooksStore[ hookName ] = { handlers: [ handler ], runs: 0, }; } if ( hookName !== 'hookAdded' ) { - doAction( 'hookAdded', hookName, namespace, callback, priority ); + hooks.doAction( + 'hookAdded', + hookName, + namespace, + callback, + priority + ); } }; } diff --git a/packages/hooks/src/createCurrentHook.js b/packages/hooks/src/createCurrentHook.js index 17f72df9e0e35..a5565dc98e2cd 100644 --- a/packages/hooks/src/createCurrentHook.js +++ b/packages/hooks/src/createCurrentHook.js @@ -3,13 +3,19 @@ * currently running hook, or `null` if no hook of the given type is currently * running. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey * * @return {() => string | null} Function that returns the current hook name or null. */ -function createCurrentHook( hooks ) { +function createCurrentHook( hooks, storeKey ) { return function currentHook() { - return hooks.__current[ hooks.__current.length - 1 ]?.name ?? null; + const hooksStore = hooks[ storeKey ]; + + return ( + hooksStore.__current[ hooksStore.__current.length - 1 ]?.name ?? + null + ); }; } diff --git a/packages/hooks/src/createDidHook.js b/packages/hooks/src/createDidHook.js index bdc3413137877..c52aafbe65635 100644 --- a/packages/hooks/src/createDidHook.js +++ b/packages/hooks/src/createDidHook.js @@ -17,18 +17,21 @@ import validateHookName from './validateHookName.js'; * Returns a function which, when invoked, will return the number of times a * hook has been called. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey * * @return {DidHook} Function that returns a hook's call count. */ -function createDidHook( hooks ) { +function createDidHook( hooks, storeKey ) { return function didHook( hookName ) { + const hooksStore = hooks[ storeKey ]; + if ( ! validateHookName( hookName ) ) { return; } - return hooks[ hookName ] && hooks[ hookName ].runs - ? hooks[ hookName ].runs + return hooksStore[ hookName ] && hooksStore[ hookName ].runs + ? hooksStore[ hookName ].runs : 0; }; } diff --git a/packages/hooks/src/createDoingHook.js b/packages/hooks/src/createDoingHook.js index 384c163a8c887..52afbce3ba497 100644 --- a/packages/hooks/src/createDoingHook.js +++ b/packages/hooks/src/createDoingHook.js @@ -12,21 +12,24 @@ * Returns a function which, when invoked, will return whether a hook is * currently being executed. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey * * @return {DoingHook} Function that returns whether a hook is currently * being executed. */ -function createDoingHook( hooks ) { +function createDoingHook( hooks, storeKey ) { return function doingHook( hookName ) { + const hooksStore = hooks[ storeKey ]; + // If the hookName was not passed, check for any current hook. if ( 'undefined' === typeof hookName ) { - return 'undefined' !== typeof hooks.__current[ 0 ]; + return 'undefined' !== typeof hooksStore.__current[ 0 ]; } // Return the __current hook. - return hooks.__current[ 0 ] - ? hookName === hooks.__current[ 0 ].name + return hooksStore.__current[ 0 ] + ? hookName === hooksStore.__current[ 0 ].name : false; }; } diff --git a/packages/hooks/src/createHasHook.js b/packages/hooks/src/createHasHook.js index 177aa8080b4e6..36a8c0450626e 100644 --- a/packages/hooks/src/createHasHook.js +++ b/packages/hooks/src/createHasHook.js @@ -13,24 +13,27 @@ * Returns a function which, when invoked, will return whether any handlers are * attached to a particular hook. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey * * @return {HasHook} Function that returns whether any handlers are * attached to a particular hook and optional namespace. */ -function createHasHook( hooks ) { +function createHasHook( hooks, storeKey ) { return function hasHook( hookName, namespace ) { + const hooksStore = hooks[ storeKey ]; + // Use the namespace if provided. if ( 'undefined' !== typeof namespace ) { return ( - hookName in hooks && - hooks[ hookName ].handlers.some( + hookName in hooksStore && + hooksStore[ hookName ].handlers.some( ( hook ) => hook.namespace === namespace ) ); } - return hookName in hooks; + return hookName in hooksStore; }; } diff --git a/packages/hooks/src/createHooks.js b/packages/hooks/src/createHooks.js index 6acdb2288405b..361383a3a97fc 100644 --- a/packages/hooks/src/createHooks.js +++ b/packages/hooks/src/createHooks.js @@ -9,37 +9,51 @@ import createCurrentHook from './createCurrentHook'; import createDoingHook from './createDoingHook'; import createDidHook from './createDidHook'; +/** + * Internal class for constructing hooks. Use `createHooks()` function + * + * Note, it is necessary to expose this class to make its type public. + * + * @private + */ +export class _Hooks { + constructor() { + /** @type {import('.').Store} actions */ + this.actions = Object.create( null ); + this.actions.__current = []; + + /** @type {import('.').Store} filters */ + this.filters = Object.create( null ); + this.filters.__current = []; + + this.addAction = createAddHook( this, 'actions' ); + this.addFilter = createAddHook( this, 'filters' ); + this.removeAction = createRemoveHook( this, 'actions' ); + this.removeFilter = createRemoveHook( this, 'filters' ); + this.hasAction = createHasHook( this, 'actions' ); + this.hasFilter = createHasHook( this, 'filters' ); + this.removeAllActions = createRemoveHook( this, 'actions', true ); + this.removeAllFilters = createRemoveHook( this, 'filters', true ); + this.doAction = createRunHook( this, 'actions' ); + this.applyFilters = createRunHook( this, 'filters', true ); + this.currentAction = createCurrentHook( this, 'actions' ); + this.currentFilter = createCurrentHook( this, 'filters' ); + this.doingAction = createDoingHook( this, 'actions' ); + this.doingFilter = createDoingHook( this, 'filters' ); + this.didAction = createDidHook( this, 'actions' ); + this.didFilter = createDidHook( this, 'filters' ); + } +} + +/** @typedef {_Hooks} Hooks */ + /** * Returns an instance of the hooks object. + * + * @return {Hooks} A Hooks instance. */ function createHooks() { - /** @type {import('.').Hooks} */ - const actions = Object.create( null ); - /** @type {import('.').Hooks} */ - const filters = Object.create( null ); - actions.__current = []; - filters.__current = []; - - return { - addAction: createAddHook( actions ), - addFilter: createAddHook( filters ), - removeAction: createRemoveHook( actions ), - removeFilter: createRemoveHook( filters ), - hasAction: createHasHook( actions ), - hasFilter: createHasHook( filters ), - removeAllActions: createRemoveHook( actions, true ), - removeAllFilters: createRemoveHook( filters, true ), - doAction: createRunHook( actions ), - applyFilters: createRunHook( filters, true ), - currentAction: createCurrentHook( actions ), - currentFilter: createCurrentHook( filters ), - doingAction: createDoingHook( actions ), - doingFilter: createDoingHook( filters ), - didAction: createDidHook( actions ), - didFilter: createDidHook( filters ), - actions, - filters, - }; + return new _Hooks(); } export default createHooks; diff --git a/packages/hooks/src/createRemoveHook.js b/packages/hooks/src/createRemoveHook.js index f76d2ea8acd8a..0b649310f1d59 100644 --- a/packages/hooks/src/createRemoveHook.js +++ b/packages/hooks/src/createRemoveHook.js @@ -3,7 +3,6 @@ */ import validateNamespace from './validateNamespace.js'; import validateHookName from './validateHookName.js'; -import { doAction } from './'; /** * @callback RemoveHook @@ -21,15 +20,18 @@ import { doAction } from './'; * Returns a function which, when invoked, will remove a specified hook or all * hooks by the given name. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. - * @param {boolean} [removeAll=false] Whether to remove all callbacks for a hookName, - * without regard to namespace. Used to create - * `removeAll*` functions. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey + * @param {boolean} [removeAll=false] Whether to remove all callbacks for a hookName, + * without regard to namespace. Used to create + * `removeAll*` functions. * * @return {RemoveHook} Function that removes hooks. */ -function createRemoveHook( hooks, removeAll = false ) { +function createRemoveHook( hooks, storeKey, removeAll = false ) { return function removeHook( hookName, namespace ) { + const hooksStore = hooks[ storeKey ]; + if ( ! validateHookName( hookName ) ) { return; } @@ -39,21 +41,21 @@ function createRemoveHook( hooks, removeAll = false ) { } // Bail if no hooks exist by this name - if ( ! hooks[ hookName ] ) { + if ( ! hooksStore[ hookName ] ) { return 0; } let handlersRemoved = 0; if ( removeAll ) { - handlersRemoved = hooks[ hookName ].handlers.length; - hooks[ hookName ] = { - runs: hooks[ hookName ].runs, + handlersRemoved = hooksStore[ hookName ].handlers.length; + hooksStore[ hookName ] = { + runs: hooksStore[ hookName ].runs, handlers: [], }; } else { // Try to find the specified callback to remove. - const handlers = hooks[ hookName ].handlers; + const handlers = hooksStore[ hookName ].handlers; for ( let i = handlers.length - 1; i >= 0; i-- ) { if ( handlers[ i ].namespace === namespace ) { handlers.splice( i, 1 ); @@ -63,7 +65,7 @@ function createRemoveHook( hooks, removeAll = false ) { // comes after the current callback, there's no problem; // otherwise we need to decrease the execution index of any // other runs by 1 to account for the removed element. - hooks.__current.forEach( ( hookInfo ) => { + hooksStore.__current.forEach( ( hookInfo ) => { if ( hookInfo.name === hookName && hookInfo.currentIndex >= i @@ -74,8 +76,9 @@ function createRemoveHook( hooks, removeAll = false ) { } } } + if ( hookName !== 'hookRemoved' ) { - doAction( 'hookRemoved', hookName, namespace ); + hooks.doAction( 'hookRemoved', hookName, namespace ); } return handlersRemoved; diff --git a/packages/hooks/src/createRunHook.js b/packages/hooks/src/createRunHook.js index 084eff8f2ed4c..da0e430871342 100644 --- a/packages/hooks/src/createRunHook.js +++ b/packages/hooks/src/createRunHook.js @@ -3,30 +3,33 @@ * registered to a hook of the specified type, optionally returning the final * value of the call chain. * - * @param {import('.').Hooks} hooks Stored hooks, keyed by hook name. - * @param {boolean} [returnFirstArg=false] Whether each hook callback is expected to - * return its first argument. + * @param {import('.').Hooks} hooks Hooks instance. + * @param {import('.').StoreKey} storeKey + * @param {boolean} [returnFirstArg=false] Whether each hook callback is expected to + * return its first argument. * * @return {(hookName:string, ...args: unknown[]) => unknown} Function that runs hook callbacks. */ -function createRunHook( hooks, returnFirstArg = false ) { +function createRunHook( hooks, storeKey, returnFirstArg = false ) { return function runHooks( hookName, ...args ) { - if ( ! hooks[ hookName ] ) { - hooks[ hookName ] = { + const hooksStore = hooks[ storeKey ]; + + if ( ! hooksStore[ hookName ] ) { + hooksStore[ hookName ] = { handlers: [], runs: 0, }; } - hooks[ hookName ].runs++; + hooksStore[ hookName ].runs++; - const handlers = hooks[ hookName ].handlers; + const handlers = hooksStore[ hookName ].handlers; // The following code is stripped from production builds. if ( 'production' !== process.env.NODE_ENV ) { // Handle any 'all' hooks registered. - if ( 'hookAdded' !== hookName && hooks.all ) { - handlers.push( ...hooks.all.handlers ); + if ( 'hookAdded' !== hookName && hooksStore.all ) { + handlers.push( ...hooksStore.all.handlers ); } } @@ -39,7 +42,7 @@ function createRunHook( hooks, returnFirstArg = false ) { currentIndex: 0, }; - hooks.__current.push( hookInfo ); + hooksStore.__current.push( hookInfo ); while ( hookInfo.currentIndex < handlers.length ) { const handler = handlers[ hookInfo.currentIndex ]; @@ -52,7 +55,7 @@ function createRunHook( hooks, returnFirstArg = false ) { hookInfo.currentIndex++; } - hooks.__current.pop(); + hooksStore.__current.pop(); if ( returnFirstArg ) { return args[ 0 ]; diff --git a/packages/hooks/src/index.js b/packages/hooks/src/index.js index 15561d318993c..68059d3e8fc66 100644 --- a/packages/hooks/src/index.js +++ b/packages/hooks/src/index.js @@ -16,7 +16,6 @@ import createHooks from './createHooks'; * @typedef Hook * @property {Handler[]} handlers Array of handlers * @property {number} runs Run counter - * */ /** @@ -26,7 +25,15 @@ import createHooks from './createHooks'; */ /** - * @typedef {Record & {__current: Current[]}} Hooks + * @typedef {Record & {__current: Current[]}} Store + */ + +/** + * @typedef {'actions' | 'filters'} StoreKey + */ + +/** + * @typedef {import('./createHooks').Hooks} Hooks */ const { diff --git a/packages/hooks/src/test/index.test.js b/packages/hooks/src/test/index.test.js index c428c1cd59dc6..da86cc5e56f21 100644 --- a/packages/hooks/src/test/index.test.js +++ b/packages/hooks/src/test/index.test.js @@ -742,6 +742,23 @@ test( 'adding an action triggers a hookAdded action passing all callback details actionA, 9 ); + + // Private instance. + const hooksPrivateInstance = createHooks(); + + removeAction( 'hookAdded', 'my_callback' ); + hookAddedSpy.mockClear(); + + hooksPrivateInstance.addAction( 'hookAdded', 'my_callback', hookAddedSpy ); + hooksPrivateInstance.addAction( 'testAction', 'my_callback2', actionA, 9 ); + + expect( hookAddedSpy ).toHaveBeenCalledTimes( 1 ); + expect( hookAddedSpy ).toHaveBeenCalledWith( + 'testAction', + 'my_callback2', + actionA, + 9 + ); } ); test( 'adding a filter triggers a hookAdded action passing all callback details', () => { @@ -757,6 +774,23 @@ test( 'adding a filter triggers a hookAdded action passing all callback details' filterA, 8 ); + + // Private instance. + const hooksPrivateInstance = createHooks(); + + removeAction( 'hookAdded', 'my_callback' ); + hookAddedSpy.mockClear(); + + hooksPrivateInstance.addAction( 'hookAdded', 'my_callback', hookAddedSpy ); + hooksPrivateInstance.addFilter( 'testFilter', 'my_callback3', filterA, 8 ); + + expect( hookAddedSpy ).toHaveBeenCalledTimes( 1 ); + expect( hookAddedSpy ).toHaveBeenCalledWith( + 'testFilter', + 'my_callback3', + filterA, + 8 + ); } ); test( 'removing an action triggers a hookRemoved action passing all callback details', () => { @@ -772,6 +806,27 @@ test( 'removing an action triggers a hookRemoved action passing all callback det 'testAction', 'my_callback2' ); + + // Private instance. + const hooksPrivateInstance = createHooks(); + + removeAction( 'hookRemoved', 'my_callback' ); + hookRemovedSpy.mockClear(); + + hooksPrivateInstance.addAction( + 'hookRemoved', + 'my_callback', + hookRemovedSpy + ); + + hooksPrivateInstance.addAction( 'testAction', 'my_callback2', actionA, 9 ); + hooksPrivateInstance.removeAction( 'testAction', 'my_callback2' ); + + expect( hookRemovedSpy ).toHaveBeenCalledTimes( 1 ); + expect( hookRemovedSpy ).toHaveBeenCalledWith( + 'testAction', + 'my_callback2' + ); } ); test( 'removing a filter triggers a hookRemoved action passing all callback details', () => { @@ -787,6 +842,27 @@ test( 'removing a filter triggers a hookRemoved action passing all callback deta 'testFilter', 'my_callback3' ); + + // Private instance. + const hooksPrivateInstance = createHooks(); + + removeAction( 'hookRemoved', 'my_callback' ); + hookRemovedSpy.mockClear(); + + hooksPrivateInstance.addAction( + 'hookRemoved', + 'my_callback', + hookRemovedSpy + ); + + hooksPrivateInstance.addFilter( 'testFilter', 'my_callback3', filterA, 8 ); + hooksPrivateInstance.removeFilter( 'testFilter', 'my_callback3' ); + + expect( hookRemovedSpy ).toHaveBeenCalledTimes( 1 ); + expect( hookRemovedSpy ).toHaveBeenCalledWith( + 'testFilter', + 'my_callback3' + ); } ); test( 'add an all filter and run it any hook to trigger it', () => { From 0d662a651c288408d78594a293b54d81b1493ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Mon, 23 Nov 2020 08:15:16 -0300 Subject: [PATCH 003/317] Full Height Alignment Toolbar: Implementation + Cover integration. (#26615) * full-height-alignment-toolbar: initial approach * block-editor: expose experimental full height * cover: add full height button to toolbar * cover: propagate full height to height input cpm * cover: dissabel unit control in full height mode * cover: store prev height to be able to return them * cover: set height via inline in full height mode * cover: add toggle control for full height * cover: set fulll height mode when saving block * full-height-alignment: change icon * cover: refactoring -> simplify full height * cover: handling not prev height values. * cover: clean fullHeightAlignment attr * full-height-align: change button label * full-height-alignment: pick icon from package * cover: improve setting prev values Props to @jorgefilipecosta --- .../README.md | 3 ++ .../index.js | 25 +++++++++++ packages/block-editor/src/components/index.js | 1 + packages/block-library/src/cover/edit.js | 45 +++++++++++++++++-- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md create mode 100644 packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js diff --git a/packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md b/packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md new file mode 100644 index 0000000000000..6febd1fc272f3 --- /dev/null +++ b/packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md @@ -0,0 +1,3 @@ +# Full Height Toolbar Toolbar + +Unlike the block alignment options, `Full Height Alignment` can be applied to a block in an independent way. But also, it works very well together with these block alignment options, where the combination empowers the design-layout capability. \ No newline at end of file diff --git a/packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js b/packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js new file mode 100644 index 0000000000000..0912a5b8fb26b --- /dev/null +++ b/packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; +import { fullscreen } from '@wordpress/icons'; + +function BlockFullHeightAlignmentToolbar( { + isActive, + label = __( 'Toggle full height' ), + onToggle, +} ) { + return ( + + onToggle( ! isActive ) } + /> + + ); +} + +export default BlockFullHeightAlignmentToolbar; diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 40c6510d292c9..150a67eb14a49 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -8,6 +8,7 @@ export * from './font-sizes'; export { default as AlignmentToolbar } from './alignment-toolbar'; export { default as Autocomplete } from './autocomplete'; export { default as BlockAlignmentToolbar } from './block-alignment-toolbar'; +export { default as __experimentalBlockFullHeightAligmentToolbar } from './block-full-height-alignment-toolbar'; export { default as __experimentalBlockAlignmentMatrixToolbar } from './block-alignment-matrix-toolbar'; export { default as BlockBreadcrumb } from './block-breadcrumb'; export { BlockContextProvider } from './block-context'; diff --git a/packages/block-library/src/cover/edit.js b/packages/block-library/src/cover/edit.js index ff9d9f8cd6eb4..00ea5c4663320 100644 --- a/packages/block-library/src/cover/edit.js +++ b/packages/block-library/src/cover/edit.js @@ -37,6 +37,7 @@ import { __experimentalPanelColorGradientSettings as PanelColorGradientSettings, __experimentalUnitControl as UnitControl, __experimentalBlockAlignmentMatrixToolbar as BlockAlignmentMatrixToolbar, + __experimentalBlockFullHeightAligmentToolbar as FullHeightAlignment, } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { withDispatch } from '@wordpress/data'; @@ -89,6 +90,7 @@ function CoverHeightInput( { value = '', } ) { const [ temporaryInput, setTemporaryInput ] = useState( null ); + const instanceId = useInstanceId( UnitControl ); const inputId = `block-cover-height-input-${ instanceId }`; const isPx = unit === 'px'; @@ -264,6 +266,39 @@ function CoverEdit( { const onSelectMedia = attributesFromMedia( setAttributes ); const isBlogUrl = isBlobURL( url ); + const [ prevMinHeightValue, setPrevMinHeightValue ] = useState( minHeight ); + const [ prevMinHeightUnit, setPrevMinHeightUnit ] = useState( + minHeightUnit + ); + const isMinFullHeight = minHeightUnit === 'vh' && minHeight === 100; + + const toggleMinFullHeight = () => { + if ( isMinFullHeight ) { + // If there aren't previous values, take the default ones. + if ( prevMinHeightUnit === 'vh' && prevMinHeightValue === 100 ) { + return setAttributes( { + minHeight: undefined, + minHeightUnit: undefined, + } ); + } + + // Set the previous values of height. + return setAttributes( { + minHeight: prevMinHeightValue, + minHeightUnit: prevMinHeightUnit, + } ); + } + + setPrevMinHeightValue( minHeight ); + setPrevMinHeightUnit( minHeightUnit ); + + // Set full height. + return setAttributes( { + minHeight: 100, + minHeightUnit: 'vh', + } ); + }; + const toggleParallax = () => { setAttributes( { hasParallax: ! hasParallax, @@ -322,6 +357,10 @@ function CoverEdit( { const controls = ( <> + { hasBackground && ( <> setAttributes( { minHeight: newMinHeight } ) } - onUnitChange={ ( nextUnit ) => { + onUnitChange={ ( nextUnit ) => setAttributes( { minHeightUnit: nextUnit, - } ); - } } + } ) + } /> Date: Mon, 23 Nov 2020 05:11:56 -0800 Subject: [PATCH 004/317] Fix RangeControl mark placement and cursor styles (#26745) * fix RangeControl mark placement/fill logic to cover more use cases * style RangeControl cursor congruent with interactive area * stop marks from interfering with input interaction * expand the story for RangeControl marks --- .../components/src/range-control/index.js | 4 +- packages/components/src/range-control/rail.js | 32 ++++----- .../src/range-control/stories/index.js | 69 ++++++++++++------- .../styles/range-control-styles.js | 12 ++-- 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/packages/components/src/range-control/index.js b/packages/components/src/range-control/index.js index 90f55c3c0cd55..543d25accfcfb 100644 --- a/packages/components/src/range-control/index.js +++ b/packages/components/src/range-control/index.js @@ -99,9 +99,7 @@ function RangeControl( const inputSliderValue = isValueReset ? '' : currentValue; - const rangeFillValue = isValueReset - ? floatClamp( max / 2, min, max ) - : value; + const rangeFillValue = isValueReset ? ( max - min ) / 2 + min : value; const calculatedFillValue = ( ( value - min ) / ( max - min ) ) * 100; const fillValue = isValueReset ? 50 : calculatedFillValue; diff --git a/packages/components/src/range-control/rail.js b/packages/components/src/range-control/rail.js index 93db3351c6812..51c188e8f4649 100644 --- a/packages/components/src/range-control/rail.js +++ b/packages/components/src/range-control/rail.js @@ -64,33 +64,33 @@ function useMarks( { marks, min = 0, max = 100, step = 1, value = 0 } ) { return []; } - const isCustomMarks = Array.isArray( marks ); - - const markCount = Math.round( ( max - min ) / step ); - const marksArray = isCustomMarks - ? marks - : [ ...Array( markCount + 1 ) ].map( ( _, index ) => ( { - value: index, - } ) ); - - const enhancedMarks = marksArray.map( ( mark, index ) => { - const markValue = mark.value !== undefined ? mark.value : value; + const range = max - min; + if ( ! Array.isArray( marks ) ) { + marks = []; + const count = 1 + Math.round( range / step ); + while ( count > marks.push( { value: step * marks.length + min } ) ); + } + const placedMarks = []; + marks.forEach( ( mark, index ) => { + if ( mark.value < min || mark.value > max ) { + return; + } const key = `mark-${ index }`; - const isFilled = markValue * step + min <= value; - const offset = `${ ( markValue / markCount ) * 100 }%`; + const isFilled = mark.value <= value; + const offset = `${ ( ( mark.value - min ) / range ) * 100 }%`; const offsetStyle = { [ isRTL ? 'right' : 'left' ]: offset, }; - return { + placedMarks.push( { ...mark, isFilled, key, style: offsetStyle, - }; + } ); } ); - return enhancedMarks; + return placedMarks; } diff --git a/packages/components/src/range-control/stories/index.js b/packages/components/src/range-control/stories/index.js index d3dca6a1ca1a3..e91fde04b8fbc 100644 --- a/packages/components/src/range-control/stories/index.js +++ b/packages/components/src/range-control/stories/index.js @@ -75,6 +75,11 @@ const DefaultExample = () => { ); }; +const RangeControlLabeledByMarksType = ( props ) => { + const label = Array.isArray( props.marks ) ? 'Custom' : 'Automatic'; + return ; +}; + export const _default = () => { return ; }; @@ -131,33 +136,51 @@ export const withReset = () => { return ; }; -export const customMarks = () => { - const marks = [ - { - value: 0, - label: '0', - }, - { - value: 1, - label: '1', - }, - { - value: 2, - label: '2', - }, - { - value: 8, - label: '8', - }, - { - value: 10, - label: '10', - }, +export const marks = () => { + const marksBase = [ + { value: 0, label: '0' }, + { value: 1, label: '1' }, + { value: 2, label: '2' }, + { value: 8, label: '8' }, + { value: 10, label: '10' }, + ]; + const marksWithDecimal = [ + ...marksBase, + { value: 3.5, label: '3.5' }, + { value: 5.8, label: '5.8' }, + ]; + const marksWithNegatives = [ + ...marksBase, + { value: -1, label: '-1' }, + { value: -2, label: '-2' }, + { value: -4, label: '-4' }, + { value: -8, label: '-8' }, ]; + const stepInteger = { min: 0, max: 10, step: 1 }; + const stepDecimal = { min: 0, max: 10, step: 0.1 }; + const minNegative = { min: -10, max: 10, step: 1 }; + const rangeNegative = { min: -10, max: -1, step: 1 }; + + // use a short alias to keep formatting to fewer lines + const Range = RangeControlLabeledByMarksType; return ( - +

Integer Step

+ + + +

Decimal Step

+ + + +

Negative Minimum

+ + + +

Negative Range

+ +
); }; diff --git a/packages/components/src/range-control/styles/range-control-styles.js b/packages/components/src/range-control/styles/range-control-styles.js index 0fa3fbbeb233d..4f49803f81d94 100644 --- a/packages/components/src/range-control/styles/range-control-styles.js +++ b/packages/components/src/range-control/styles/range-control-styles.js @@ -11,11 +11,11 @@ import NumberControl from '../../number-control'; import { color, reduceMotion, rtl, space } from '../../utils/style-mixins'; const rangeHeight = () => css( { height: 30, minHeight: 30 } ); +const thumbSize = 20; export const Root = styled.span` -webkit-tap-highlight-color: transparent; box-sizing: border-box; - cursor: pointer; align-items: flex-start; display: inline-flex; justify-content: flex-start; @@ -115,6 +115,7 @@ export const Track = styled.span` export const MarksWrapper = styled.span` box-sizing: border-box; display: block; + pointer-events: none; position: relative; width: 100%; user-select: none; @@ -166,7 +167,7 @@ export const ThumbWrapper = styled.span` align-items: center; box-sizing: border-box; display: flex; - height: 20px; + height: ${ thumbSize }px; justify-content: center; margin-top: 5px; outline: 0; @@ -174,7 +175,7 @@ export const ThumbWrapper = styled.span` position: absolute; top: 0; user-select: none; - width: 20px; + width: ${ thumbSize }px; ${ rtl( { marginLeft: -10 } ) } `; @@ -202,7 +203,6 @@ export const Thumb = styled.span` box-sizing: border-box; height: 100%; outline: 0; - pointer-events: none; position: absolute; user-select: none; width: 100%; @@ -216,13 +216,13 @@ export const InputRange = styled.input` display: block; height: 100%; left: 0; - margin: 0; + margin: 0 -${ thumbSize / 2 }px; opacity: 0; outline: none; position: absolute; right: 0; top: 0; - width: 100%; + width: calc( 100% + ${ thumbSize }px ); `; const tooltipShow = ( { show } ) => { From 39dbdd2a90d7444259ba3f4756217557cf88b4d4 Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Mon, 23 Nov 2020 06:01:38 -0800 Subject: [PATCH 005/317] Configure phpunit-watcher to improve devex. (#27058) This provides similar features for PHP tests that Jest provides for JavaScript tests. The `process-timeout` option is disabled so that phpunit-watcher can run indefinitely. See https://github.com/spatie/phpunit-watcher/issues/63#issuecomment-545633709". --- .gitignore | 2 + composer.json | 11 +- composer.lock | 3173 ++++++++++++++++++++++++- docs/contributors/testing-overview.md | 8 + package.json | 1 + phpunit-watcher.yml.dist | 8 + 6 files changed, 3083 insertions(+), 120 deletions(-) create mode 100644 phpunit-watcher.yml.dist diff --git a/.gitignore b/.gitignore index f27befbdcebd7..fc51886c3087d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ test/native/junit.xml # Local overrides .wp-env.override.json +phpunit.xml +phpunit-watcher.yml diff --git a/composer.json b/composer.json index 49d6605108cfe..2a16d1accff3e 100644 --- a/composer.json +++ b/composer.json @@ -10,19 +10,26 @@ "support": { "issues": "https://github.com/WordPress/gutenberg/issues" }, + "config": { + "process-timeout": 0 + }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7", "squizlabs/php_codesniffer": "^3.5", "phpcompatibility/php-compatibility": "^9.3", "wp-coding-standards/wpcs": "^2.2", "sirbrillig/phpcs-variable-analysis": "^2.8", - "wp-phpunit/wp-phpunit": "^5.4" + "wp-phpunit/wp-phpunit": "^5.4", + "phpunit/phpunit": "^7.5.20", + "spatie/phpunit-watcher": "^1.23" }, "require": { "composer/installers": "~1.0" }, "scripts": { "format": "phpcbf --standard=phpcs.xml.dist --report-summary --report-source", - "lint": "phpcs --standard=phpcs.xml.dist --runtime-set ignore_warnings_on_exit 1" + "lint": "phpcs --standard=phpcs.xml.dist --runtime-set ignore_warnings_on_exit 1", + "test": "phpunit", + "test:watch": "phpunit-watcher watch < /dev/tty" } } diff --git a/composer.lock b/composer.lock index ca3dde5106f29..bff5a02fc188c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "693be7bb8e616544f4b253f4a2168e04", + "content-hash": "a0cee819dff559c9839fd0e98cb4186a", "packages": [ { "name": "composer/installers", @@ -145,37 +145,2824 @@ } ], "packages-dev": [ + { + "name": "clue/stdio-react", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-stdio.git", + "reference": "5f42a3a5a29f52432f0f68b57890353e8ca65069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-stdio/zipball/5f42a3a5a29f52432f0f68b57890353e8ca65069", + "reference": "5f42a3a5a29f52432f0f68b57890353e8ca65069", + "shasum": "" + }, + "require": { + "clue/term-react": "^1.0 || ^0.1.1", + "clue/utf8-react": "^1.0 || ^0.1", + "php": ">=5.3", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", + "react/stream": "^1.0 || ^0.7 || ^0.6" + }, + "require-dev": { + "clue/arguments": "^2.0", + "clue/commander": "^1.2", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-mbstring": "Using ext-mbstring should provide slightly better performance for handling I/O" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Stdio\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "Async, event-driven console input & output (STDIN, STDOUT) for truly interactive CLI applications, built on top of ReactPHP", + "homepage": "https://github.com/clue/reactphp-stdio", + "keywords": [ + "async", + "autocomplete", + "autocompletion", + "cli", + "history", + "interactive", + "reactphp", + "readline", + "stdin", + "stdio", + "stdout" + ], + "time": "2019-08-28T12:01:30+00:00" + }, + { + "name": "clue/term-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-term.git", + "reference": "eb6eb063eda04a714ef89f066586a2c49588f7ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-term/zipball/eb6eb063eda04a714ef89f066586a2c49588f7ca", + "reference": "eb6eb063eda04a714ef89f066586a2c49588f7ca", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.0 || ^0.7" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Term\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming terminal emulator, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-term", + "keywords": [ + "C0", + "CSI", + "ansi", + "apc", + "ascii", + "c1", + "control codes", + "dps", + "osc", + "pm", + "reactphp", + "streaming", + "terminal", + "vt100", + "xterm" + ], + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-11-06T11:50:12+00:00" + }, + { + "name": "clue/utf8-react", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-utf8.git", + "reference": "8bc3f8c874cdf642c8f10f9ae93aadb8cd63da96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-utf8/zipball/8bc3f8c874cdf642c8f10f9ae93aadb8cd63da96", + "reference": "8bc3f8c874cdf642c8f10f9ae93aadb8cd63da96", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4 || ^0.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 ||^5.7 || ^4.8", + "react/stream": "^1.0 || ^0.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\Utf8\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming UTF-8 parser, built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-utf8", + "keywords": [ + "reactphp", + "streaming", + "unicode", + "utf-8", + "utf8" + ], + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2020-11-06T11:48:09+00:00" + }, { "name": "dealerdirect/phpcodesniffer-composer-installer", "version": "v0.7.0", "source": { "type": "git", - "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7" + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "e8d808670b8f882188368faaf1144448c169c0b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", + "reference": "e8d808670b8f882188368faaf1144448c169c0b7", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" + }, + "require-dev": { + "composer/composer": "*", + "phpcompatibility/php-compatibility": "^9.0", + "sensiolabs/security-checker": "^4.1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://www.dealerdirect.com", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "time": "2020-06-25T14:57:39+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^8.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-11-10T18:47:58+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.1", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Evenement": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "time": "2017-07-23T21:35:13+00:00" + }, + { + "name": "jolicode/jolinotif", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/jolicode/JoliNotif.git", + "reference": "52f5b98f964f6009b8ec4c0e951edcd0862e2ac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jolicode/JoliNotif/zipball/52f5b98f964f6009b8ec4c0e951edcd0862e2ac7", + "reference": "52f5b98f964f6009b8ec4c0e951edcd0862e2ac7", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "symfony/process": "^3.3|^4.0|^5.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.0", + "symfony/finder": "^3.3|^4.0|^5.0", + "symfony/phpunit-bridge": "^3.4.26|^4.0|^5.0" + }, + "bin": [ + "jolinotif" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Joli\\JoliNotif\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Loïck Piera", + "email": "pyrech@gmail.com" + } + ], + "description": "Send desktop notifications on Windows, Linux, MacOS.", + "keywords": [ + "MAC", + "growl", + "linux", + "notification", + "windows" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/jolicode/jolinotif", + "type": "tidelift" + } + ], + "time": "2020-06-17T08:25:38+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-11-13T09:40:50+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "time": "2019-12-27T09:44:58+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.2.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", + "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-09-03T19:13:55+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-09-17T18:55:26+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0 || ^9.0 <9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-09-29T09:10:42+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "6.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.1", + "phpunit/php-file-iterator": "^2.0", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.1 || ^4.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "suggest": { + "ext-xdebug": "^2.6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2018-10-31T16:06:48+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "050bedf145a257b1ff02746c31894800e5122946" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2018-09-13T20:33:42+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "2.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2019-06-07T04:22:29+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "abandoned": true, + "time": "2019-09-17T06:23:10+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "7.5.20", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", + "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "myclabs/deep-copy": "^1.7", + "phar-io/manifest": "^1.0.2", + "phar-io/version": "^2.0", + "php": "^7.1", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^6.0.7", + "phpunit/php-file-iterator": "^2.0.1", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1", + "sebastian/comparator": "^3.0", + "sebastian/diff": "^3.0", + "sebastian/environment": "^4.0", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0", + "sebastian/version": "^2.0.1" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "*" + }, + "require-dev": { + "ext-pdo": "*" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.5-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2020-01-08T08:45:45+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "6d24de090cd59cfc830263cfba965be77b563c13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/6d24de090cd59cfc830263cfba965be77b563c13", + "reference": "6d24de090cd59cfc830263cfba965be77b563c13", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-event": "~1.0 for ExtEventLoop", + "ext-pcntl": "For signal handling support when using the StreamSelectLoop", + "ext-uv": "* for ExtUvLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "time": "2020-01-01T18:39:52+00:00" + }, + { + "name": "react/stream", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "reference": "7c02b510ee3f582c810aeccd3a197b9c2f52ff1a", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "time": "2020-05-04T10:17:57+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "time": "2017-03-04T06:30:41+00:00" + }, + { + "name": "sebastian/comparator", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "shasum": "" + }, + "require": { + "php": "^7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2018-07-12T15:12:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "time": "2019-02-04T06:01:07+00:00" + }, + { + "name": "sebastian/environment", + "version": "4.2.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2019-11-20T08:46:58+00:00" + }, + { + "name": "sebastian/exporter", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2019-09-14T09:02:43+00:00" + }, + { + "name": "sebastian/global-state", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2017-04-27T15:39:26+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "shasum": "" + }, + "require": { + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "time": "2017-08-03T12:35:26+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2017-03-03T06:23:57+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2016-10-03T07:35:21+00:00" + }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "ff54d4ec7f2bd152d526fdabfeff639aa9b8be01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/ff54d4ec7f2bd152d526fdabfeff639aa9b8be01", + "reference": "ff54d4ec7f2bd152d526fdabfeff639aa9b8be01", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.1" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4 || ^0.5 || ^0.6", + "limedeck/phpunit-detailed-printer": "^3.1 || ^4.0 || ^5.0", + "phpstan/phpstan": "^0.11.8", + "phpunit/phpunit": "^5.0 || ^6.5 || ^7.0 || ^8.0", + "sirbrillig/phpcs-import-detection": "^1.1" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "time": "2020-10-07T23:32:29+00:00" + }, + { + "name": "spatie/phpunit-watcher", + "version": "1.23.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/phpunit-watcher.git", + "reference": "8a8e0c3c8f3f03dfdb6bf62abf89c1b7273fc0b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/phpunit-watcher/zipball/8a8e0c3c8f3f03dfdb6bf62abf89c1b7273fc0b3", + "reference": "8a8e0c3c8f3f03dfdb6bf62abf89c1b7273fc0b3", + "shasum": "" + }, + "require": { + "clue/stdio-react": "^2.0", + "jolicode/jolinotif": "^2.0", + "php": "^7.2", + "symfony/console": "^4.0|^5.0", + "symfony/process": "^4.0|^5.0", + "symfony/yaml": "^4.0|^5.0", + "yosymfony/resource-watcher": "^2.0" + }, + "conflict": { + "yosymfony/resource-watcher": "<2.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0" + }, + "bin": [ + "phpunit-watcher" + ], + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\PhpUnitWatcher\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Automatically rerun PHPUnit tests when source code changes", + "homepage": "https://github.com/spatie/phpunit-watcher", + "keywords": [ + "phpunit-watcher", + "spatie" + ], + "time": "2020-10-27T07:36:25+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.8", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", + "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2020-10-23T02:01:07+00:00" + }, + { + "name": "symfony/console", + "version": "v5.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" + }, + "conflict": { + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" + }, + { + "name": "symfony/finder", + "version": "v5.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "reference": "e70eb5a69c2ff61ea135a13d2266e8914a67b3a0", + "shasum": "" + }, + "require": { + "php": ">=7.2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "727d1096295d807c309fb01a851577302394c897" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, + { + "name": "symfony/process", + "version": "v5.1.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f00872c3f6804150d6a0f73b4151daab96248101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f00872c3f6804150d6a0f73b4151daab96248101", + "reference": "f00872c3f6804150d6a0f73b4151daab96248101", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.15" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" + "php": ">=7.2.5", + "psr/container": "^1.0" }, - "require-dev": { - "composer/composer": "*", - "phpcompatibility/php-compatibility": "^9.0", - "sensiolabs/security-checker": "^4.1.0" + "suggest": { + "symfony/service-implementation": "" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + "branch-alias": { + "dev-master": "2.2-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } }, "autoload": { "psr-4": { - "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + "Symfony\\Contracts\\Service\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -184,189 +2971,286 @@ ], "authors": [ { - "name": "Franck Nijhof", - "email": "franck.nijhof@dealerdirect.com", - "homepage": "http://www.frenck.nl", - "role": "Developer / IT Manager" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://www.dealerdirect.com", + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], - "time": "2020-06-25T14:57:39+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-09-07T11:33:47+00:00" }, { - "name": "phpcompatibility/php-compatibility", - "version": "9.3.5", + "name": "symfony/string", + "version": "v5.1.8", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + "url": "https://github.com/symfony/string.git", + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", - "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "url": "https://api.github.com/repos/symfony/string/zipball/a97573e960303db71be0dd8fda9be3bca5e0feea", + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" - }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] }, - "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "MIT" ], "authors": [ { - "name": "Wim Godden", - "homepage": "https://github.com/wimg", - "role": "lead" - }, - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Contributors", - "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "description": "Symfony String component", + "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "phpcs", - "standards" + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" ], - "time": "2019-12-27T09:44:58+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:01:57+00:00" }, { - "name": "sirbrillig/phpcs-variable-analysis", - "version": "v2.9.0", + "name": "symfony/yaml", + "version": "v5.1.8", "source": { "type": "git", - "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", - "reference": "ff54d4ec7f2bd152d526fdabfeff639aa9b8be01" + "url": "https://github.com/symfony/yaml.git", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/ff54d4ec7f2bd152d526fdabfeff639aa9b8be01", - "reference": "ff54d4ec7f2bd152d526fdabfeff639aa9b8be01", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6", "shasum": "" }, "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.1" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/console": "<4.4" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4 || ^0.5 || ^0.6", - "limedeck/phpunit-detailed-printer": "^3.1 || ^4.0 || ^5.0", - "phpstan/phpstan": "^0.11.8", - "phpunit/phpunit": "^5.0 || ^6.5 || ^7.0 || ^8.0", - "sirbrillig/phpcs-import-detection": "^1.1" + "symfony/console": "^4.4|^5.0" }, - "type": "phpcodesniffer-standard", + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", "autoload": { "psr-4": { - "VariableAnalysis\\": "VariableAnalysis/" - } + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "MIT" ], "authors": [ { - "name": "Sam Graham", - "email": "php-codesniffer-variableanalysis@illusori.co.uk" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Payton Swick", - "email": "payton@foolord.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A PHPCS sniff to detect problems with variables.", - "time": "2020-10-07T23:32:29+00:00" + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-24T12:03:25+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.5.8", + "name": "theseer/tokenizer", + "version": "1.2.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", "shasum": "" }, "require": { - "ext-simplexml": "*", + "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": "^7.2 || ^8.0" }, - "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Greg Sherwood", - "role": "lead" + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "description": "Assertions to validate method input/output with nice error messages.", "keywords": [ - "phpcs", - "standards" + "assert", + "check", + "validate" ], - "time": "2020-10-23T02:01:07+00:00" + "time": "2020-07-08T17:02:28+00:00" }, { "name": "wp-coding-standards/wpcs", @@ -416,7 +3300,7 @@ }, { "name": "wp-phpunit/wp-phpunit", - "version": "5.5.1", + "version": "5.5.3", "source": { "type": "git", "url": "https://github.com/wp-phpunit/wp-phpunit.git", @@ -456,6 +3340,59 @@ "wordpress" ], "time": "2020-09-02T15:53:50+00:00" + }, + { + "name": "yosymfony/resource-watcher", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/yosymfony/resource-watcher.git", + "reference": "a8c34f704e6bd4f786c97f3c0ba65bd86cb2bd73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yosymfony/resource-watcher/zipball/a8c34f704e6bd4f786c97f3c0ba65bd86cb2bd73", + "reference": "a8c34f704e6bd4f786c97f3c0ba65bd86cb2bd73", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "symfony/finder": "^2.7|^3.0|^4.0|^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "symfony/filesystem": "^2.7|^3.0|^4.0|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Yosymfony\\ResourceWatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Victor Puertas", + "email": "vpgugr@gmail.com" + } + ], + "description": "A simple resource watcher using Symfony Finder", + "homepage": "http://yosymfony.com", + "keywords": [ + "finder", + "resources", + "symfony", + "watcher" + ], + "time": "2020-01-04T15:36:55+00:00" } ], "aliases": [], diff --git a/docs/contributors/testing-overview.md b/docs/contributors/testing-overview.md index c48360190946f..c5d21a281e5df 100644 --- a/docs/contributors/testing-overview.md +++ b/docs/contributors/testing-overview.md @@ -461,8 +461,16 @@ Tests for PHP use [PHPUnit](https://phpunit.de/) as the testing framework. If yo npm run test-php ``` +To re-run tests automatically when files change (similar to Jest), run: + +``` +npm run test-php:watch +``` + _Note: The phpunit commands require `wp-env` to be running and composer dependencies to be installed. The package script will start wp-env for you if it is not already running._ +In other environments, run `composer run test` and `composer run test:watch`. + Code style in PHP is enforced using [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer). It is recommended that you install PHP_CodeSniffer and the [WordPress Coding Standards for PHP_CodeSniffer](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards#installation) ruleset using [Composer](https://getcomposer.org/). With Composer installed, run `composer install` from the project directory to install dependencies. The above `npm run test-php` will execute both unit tests and code linting. Code linting can be verified independently by running `npm run lint-php`. To run unit tests only, without the linter, use `npm run test-unit-php` instead. diff --git a/package.json b/package.json index b175c86348462..9dc22cfc196e6 100644 --- a/package.json +++ b/package.json @@ -250,6 +250,7 @@ "test-e2e:watch": "npm run test-e2e -- --watch", "test-performance": "wp-scripts test-e2e --config packages/e2e-tests/jest.performance.config.js", "test-php": "npm run lint-php && npm run test-unit-php", + "test-php:watch": "wp-env run composer run-script test:watch", "test-unit": "wp-scripts test-unit-js --config test/unit/jest.config.js", "test-unit:debug": "wp-scripts --inspect-brk test-unit-js --runInBand --no-cache --verbose --config test/unit/jest.config.js ", "test-unit:update": "npm run test-unit -- --updateSnapshot", diff --git a/phpunit-watcher.yml.dist b/phpunit-watcher.yml.dist new file mode 100644 index 0000000000000..f2f9da5fbcdbf --- /dev/null +++ b/phpunit-watcher.yml.dist @@ -0,0 +1,8 @@ +watch: + directories: + - ./lib/ + - ./phpunit/ + +notifications: + passingTests: false + failingTests: false From 9b255cfa2b8111e03bbb7077929e0fd57e6c71bb Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Mon, 23 Nov 2020 09:50:10 -0500 Subject: [PATCH 006/317] Update link to react-native-editor device tests contributing guide (#27200) --- packages/react-native-editor/__device-tests__/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-editor/__device-tests__/README.md b/packages/react-native-editor/__device-tests__/README.md index 0d7a11cbd38be..030f2947dc744 100644 --- a/packages/react-native-editor/__device-tests__/README.md +++ b/packages/react-native-editor/__device-tests__/README.md @@ -67,4 +67,4 @@ After the build is complete, an appium server is fired up on port 4723 and the d ----- -To read more about writing your own tests please read the [contributing guide](https://github.com/wordpress-mobile/gutenberg-mobile/blob/develop/__device-tests__/CONTRIBUTING.md) +To read more about writing your own tests please read the [contributing guide](https://github.com/WordPress/gutenberg/blob/master/packages/react-native-editor/__device-tests__/CONTRIBUTING.md) From 8ae67e79e470555046e29468378d493b14bda8bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Mon, 23 Nov 2020 16:03:46 +0100 Subject: [PATCH 007/317] ESLint Plugin: Include a note about the minimum version required for node and npm (#27203) --- packages/eslint-plugin/CHANGELOG.md | 4 +++ packages/eslint-plugin/README.md | 50 +++++++++++++++-------------- packages/eslint-plugin/package.json | 4 +++ 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index fbe29bb0447f9..a977dccfde44c 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Documentation + +- Include a note about the minimum version required for `node` (10.0.0) and `npm` (6.9.0). + ## 7.2.1 (2020-09-17) ### Bug Fixes diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index a47c932d40354..cd53db818d977 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -10,6 +10,8 @@ Install the module npm install @wordpress/eslint-plugin --save-dev ``` +**Note**: This package requires `node` 10.0.0 or later, and `npm` 6.9.0 or later. It is not compatible with older versions. + ## Usage To opt-in to the default configuration, extend your own project's `.eslintrc` file: @@ -30,15 +32,15 @@ There is also `recommended-with-formatting` ruleset for projects that want to op Alternatively, you can opt-in to only the more granular rulesets offered by the plugin. These include: -- `custom` -- `es5` -- `esnext` -- `jsdoc` -- `jsx-a11y` -- `react` -- `i18n` -- `test-e2e` -- `test-unit` +- `custom` +- `es5` +- `esnext` +- `jsdoc` +- `jsx-a11y` +- `react` +- `i18n` +- `test-e2e` +- `test-unit` For example, if your project does not use React, you could consider extending including only the ESNext rules in your project using the following `extends` definition: @@ -54,21 +56,21 @@ The granular rulesets will not define any environment globals. As such, if they ### Rules -Rule|Description|Recommended ----|---|--- -[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|✓ -[react-no-unsafe-timeout](/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md)|Disallow unsafe `setTimeout` in component| -[valid-sprintf](/packages/eslint-plugin/docs/rules/valid-sprintf.md)|Enforce valid sprintf usage|✓ -[no-base-control-with-label-without-id](/packages/eslint-plugin/docs/rules/no-base-control-with-label-without-id.md)| Disallow the usage of BaseControl component with a label prop set but omitting the id property|✓ -[no-unguarded-get-range-at](/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md)|Disallow the usage of unguarded `getRangeAt` calls|✓ -[i18n-ellipsis](/packages/eslint-plugin/docs/rules/i18n-ellipsis.md)|Disallow using three dots in translatable strings|✓ -[i18n-no-collapsible-whitespace](/packages/eslint-plugin/docs/rules/i18n-no-collapsible-whitespace.md)|Disallow collapsible whitespace in translatable strings|✓ -[i18n-no-placeholders-only](/packages/eslint-plugin/docs/rules/i18n-no-placeholders-only.md)|Prevent using only placeholders in translatable strings|✓ -[i18n-no-variables](/packages/eslint-plugin/docs/rules/i18n-no-variables.md)|Enforce string literals as translation function arguments|✓ -[i18n-text-domain](/packages/eslint-plugin/docs/rules/i18n-text-domain.md)|Enforce passing valid text domains|✓ -[i18n-translator-comments](/packages/eslint-plugin/docs/rules/i18n-translator-comments.md)|Enforce adding translator comments|✓ +| Rule | Description | Recommended | +| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ----------- | +| [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 | ✓ | +| [react-no-unsafe-timeout](/packages/eslint-plugin/docs/rules/react-no-unsafe-timeout.md) | Disallow unsafe `setTimeout` in component | +| [valid-sprintf](/packages/eslint-plugin/docs/rules/valid-sprintf.md) | Enforce valid sprintf usage | ✓ | +| [no-base-control-with-label-without-id](/packages/eslint-plugin/docs/rules/no-base-control-with-label-without-id.md) | Disallow the usage of BaseControl component with a label prop set but omitting the id property | ✓ | +| [no-unguarded-get-range-at](/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md) | Disallow the usage of unguarded `getRangeAt` calls | ✓ | +| [i18n-ellipsis](/packages/eslint-plugin/docs/rules/i18n-ellipsis.md) | Disallow using three dots in translatable strings | ✓ | +| [i18n-no-collapsible-whitespace](/packages/eslint-plugin/docs/rules/i18n-no-collapsible-whitespace.md) | Disallow collapsible whitespace in translatable strings | ✓ | +| [i18n-no-placeholders-only](/packages/eslint-plugin/docs/rules/i18n-no-placeholders-only.md) | Prevent using only placeholders in translatable strings | ✓ | +| [i18n-no-variables](/packages/eslint-plugin/docs/rules/i18n-no-variables.md) | Enforce string literals as translation function arguments | ✓ | +| [i18n-text-domain](/packages/eslint-plugin/docs/rules/i18n-text-domain.md) | Enforce passing valid text domains | ✓ | +| [i18n-translator-comments](/packages/eslint-plugin/docs/rules/i18n-translator-comments.md) | Enforce adding translator comments | ✓ | ### Legacy diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 4b03086220c42..ed1b35fc55ef2 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -19,6 +19,10 @@ "bugs": { "url": "https://github.com/WordPress/gutenberg/issues" }, + "engines": { + "node": ">=10", + "npm": ">=6.9" + }, "files": [ "configs", "rules", From 78bb110687bf247ab3ee8f07818ced4913909eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Mon, 23 Nov 2020 16:12:32 +0100 Subject: [PATCH 008/317] Scripts: Unpin ignore-emit-webpack-plugin dependency (#26739) Now that ignore-emit-webpack-plugin was fixed to work with both webpack v4 and v5 we can allow again to use ranges for the dependency. --- package-lock.json | 8 ++++---- packages/scripts/CHANGELOG.md | 4 ++++ packages/scripts/package.json | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83e461e91fda3..d9fe22a399a14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18276,7 +18276,7 @@ "dir-glob": "^3.0.1", "eslint": "^7.1.0", "eslint-plugin-markdown": "^1.0.2", - "ignore-emit-webpack-plugin": "2.0.3", + "ignore-emit-webpack-plugin": "^2.0.6", "jest": "^25.3.0", "jest-puppeteer": "^4.4.0", "markdownlint": "^0.18.0", @@ -38282,9 +38282,9 @@ "dev": true }, "ignore-emit-webpack-plugin": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/ignore-emit-webpack-plugin/-/ignore-emit-webpack-plugin-2.0.3.tgz", - "integrity": "sha512-ahTYD5KZ3DiZG9goS8NCxBaPEfXsPLH5JeWKmFTThD8lsPen6R4tLnWcN/mrksK5cDqyxOzmRL12feJQZjffuA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/ignore-emit-webpack-plugin/-/ignore-emit-webpack-plugin-2.0.6.tgz", + "integrity": "sha512-/zC18RWCC2wz4ZwnS4UoujGWzvSKy28DLjtE+jrGBOXej6YdmityhBDzE8E0NlktEqi4tgdNbydX8B6G4haHSQ==", "dev": true }, "ignore-walk": { diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index ebd28463357d5..2585c2d82ea5d 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -6,6 +6,10 @@ - Autoformat TypeScript files (`*.ts` and `*.tsx`) in `format-js` script (#27138)[https://github.com/WordPress/gutenberg/pull/27138]. +### Internal + +- The bundled `ignore-emit-webpack-plugin` dependency has been updated from requiring `2.0.3` to requiring `^2.0.6`. + ## 12.5.0 (2020-10-30) ### Enhancements diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 5bda105e84d1d..9e02bcc2f0205 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -50,7 +50,7 @@ "dir-glob": "^3.0.1", "eslint": "^7.1.0", "eslint-plugin-markdown": "^1.0.2", - "ignore-emit-webpack-plugin": "2.0.3", + "ignore-emit-webpack-plugin": "^2.0.6", "jest": "^25.3.0", "jest-puppeteer": "^4.4.0", "markdownlint": "^0.18.0", From 037276c880ed38702ce6cc7ff9c35a8c165263ee Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 23 Nov 2020 16:15:52 +0100 Subject: [PATCH 009/317] Revert stan (#27196) --- .gitignore | 2 +- .../data/data-core-keyboard-shortcuts.md | 20 +- docs/manifest.json | 6 - package-lock.json | 11 - package.json | 1 - .../src/store/test/selectors.js | 26 +- packages/compose/src/index.native.js | 1 - packages/data/package.json | 1 - packages/data/src/atomic-store/index.js | 108 -------- .../data/src/components/use-select/index.js | 164 +++++++---- .../src/components/use-select/test/index.js | 31 +-- .../src/components/with-select/test/index.js | 105 +++++-- packages/data/src/factory.js | 4 +- packages/data/src/index.js | 1 - packages/data/src/redux-store/index.js | 262 ++++++++++-------- packages/data/src/redux-store/test/index.js | 171 +----------- packages/data/src/registry.js | 52 +--- packages/data/src/types.d.ts | 32 +++ packages/data/src/types.ts | 75 ----- packages/data/tsconfig.json | 17 -- .../lib/util.js | 6 +- .../specs/editor/various/typewriter.test.js | 24 -- .../src/store/test/selectors.js | 70 +++-- .../test/listener-hooks.js | 35 ++- .../edit-site/src/store/test/selectors.js | 8 +- .../editor/src/components/post-title/index.js | 2 +- packages/editor/src/store/actions.js | 2 - packages/editor/src/store/test/selectors.js | 7 +- packages/keyboard-shortcuts/package.json | 1 - .../keyboard-shortcuts/src/store/actions.js | 69 ++--- .../keyboard-shortcuts/src/store/atoms.js | 20 -- .../keyboard-shortcuts/src/store/index.js | 19 +- .../keyboard-shortcuts/src/store/reducer.js | 33 +++ .../keyboard-shortcuts/src/store/selectors.js | 101 +++---- packages/redux-routine/src/runtime.js | 1 - packages/stan/.npmrc | 1 - packages/stan/CHANGELOG.md | 5 - packages/stan/README.md | 216 --------------- packages/stan/package.json | 37 --- packages/stan/src/atom.js | 44 --- packages/stan/src/derived.js | 203 -------------- packages/stan/src/index.js | 25 -- packages/stan/src/registry.js | 151 ---------- packages/stan/src/selector.js | 55 ---- packages/stan/src/store.js | 83 ------ packages/stan/src/test/atom.js | 28 -- packages/stan/src/test/derived.js | 169 ----------- packages/stan/src/test/selector.js | 195 ------------- packages/stan/src/types.ts | 130 --------- packages/stan/tsconfig.json | 11 - tsconfig.base.json | 3 - tsconfig.json | 2 - webpack.config.js | 6 +- 53 files changed, 609 insertions(+), 2243 deletions(-) delete mode 100644 packages/data/src/atomic-store/index.js create mode 100644 packages/data/src/types.d.ts delete mode 100644 packages/data/src/types.ts delete mode 100644 packages/data/tsconfig.json delete mode 100644 packages/keyboard-shortcuts/src/store/atoms.js create mode 100644 packages/keyboard-shortcuts/src/store/reducer.js delete mode 100644 packages/stan/.npmrc delete mode 100644 packages/stan/CHANGELOG.md delete mode 100644 packages/stan/README.md delete mode 100644 packages/stan/package.json delete mode 100644 packages/stan/src/atom.js delete mode 100644 packages/stan/src/derived.js delete mode 100644 packages/stan/src/index.js delete mode 100644 packages/stan/src/registry.js delete mode 100644 packages/stan/src/selector.js delete mode 100644 packages/stan/src/store.js delete mode 100644 packages/stan/src/test/atom.js delete mode 100644 packages/stan/src/test/derived.js delete mode 100644 packages/stan/src/test/selector.js delete mode 100644 packages/stan/src/types.ts delete mode 100644 packages/stan/tsconfig.json diff --git a/.gitignore b/.gitignore index fc51886c3087d..cf7a035b43b06 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ playground/dist .cache *.tsbuildinfo -# Report generated from tests +# Report generated from jest-junit test/native/junit.xml # Local overrides diff --git a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md index 1cb83fc4c26d2..fefa8e189e215 100644 --- a/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md +++ b/docs/designers-developers/developers/data/data-core-keyboard-shortcuts.md @@ -12,6 +12,7 @@ Returns the raw representation of all the keyboard combinations of a given short _Parameters_ +- _state_ `Object`: Global state. - _name_ `string`: Shortcut name. _Returns_ @@ -24,7 +25,8 @@ Returns the shortcut names list for a given category name. _Parameters_ -- _categoryName_ `string`: Category name. +- _state_ `Object`: Global state. +- _name_ `string`: Category name. _Returns_ @@ -36,6 +38,7 @@ Returns the aliases for a given shortcut name. _Parameters_ +- _state_ `Object`: Global state. - _name_ `string`: Shortcut name. _Returns_ @@ -48,6 +51,7 @@ Returns the shortcut description given its name. _Parameters_ +- _state_ `Object`: Global state. - _name_ `string`: Shortcut name. _Returns_ @@ -60,6 +64,7 @@ Returns the main key combination for a given shortcut name. _Parameters_ +- _state_ `Object`: Global state. - _name_ `string`: Shortcut name. _Returns_ @@ -72,6 +77,7 @@ Returns a string representing the main key combination for a given shortcut name _Parameters_ +- _state_ `Object`: Global state. - _name_ `string`: Shortcut name. - _representation_ (unknown type): Type of representation (display, raw, ariaLabel). @@ -91,19 +97,23 @@ Returns an action object used to register a new keyboard shortcut. _Parameters_ -- _get_ `Function`: Atom resover. -- _set_ `Function`: Atom updater. - _config_ `WPShortcutConfig`: Shortcut config. +_Returns_ + +- `Object`: action. + # **unregisterShortcut** Returns an action object used to unregister a keyboard shortcut. _Parameters_ -- _get_ `Function`: get atom value. -- _set_ `Function`: set atom value. - _name_ `string`: Shortcut name. +_Returns_ + +- `Object`: action. + diff --git a/docs/manifest.json b/docs/manifest.json index 61e72bc0a2039..dee68b8b1e8d3 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1793,12 +1793,6 @@ "markdown_source": "../packages/shortcode/README.md", "parent": "packages" }, - { - "title": "@wordpress/stan", - "slug": "packages-stan", - "markdown_source": "../packages/stan/README.md", - "parent": "packages" - }, { "title": "@wordpress/token-list", "slug": "packages-token-list", diff --git a/package-lock.json b/package-lock.json index d9fe22a399a14..9b0cd4a7d26ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17545,7 +17545,6 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/redux-routine": "file:packages/redux-routine", - "@wordpress/stan": "file:packages/stan", "equivalent-key-map": "^0.2.2", "is-promise": "^4.0.0", "lodash": "^4.17.19", @@ -17993,7 +17992,6 @@ "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", "@wordpress/keycodes": "file:packages/keycodes", - "@wordpress/stan": "file:packages/stan", "lodash": "^4.17.19", "rememo": "^3.0.0" } @@ -18325,15 +18323,6 @@ "memize": "^1.1.0" } }, - "@wordpress/stan": { - "version": "file:packages/stan", - "requires": { - "@babel/runtime": "^7.11.2", - "@wordpress/priority-queue": "file:packages/priority-queue", - "equivalent-key-map": "^0.2.2", - "lodash": "^4.17.19" - } - }, "@wordpress/token-list": { "version": "file:packages/token-list", "requires": { diff --git a/package.json b/package.json index 9dc22cfc196e6..6cea6b4344b34 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/shortcode": "file:packages/shortcode", - "@wordpress/stan": "file:packages/stan", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", diff --git a/packages/block-directory/src/store/test/selectors.js b/packages/block-directory/src/store/test/selectors.js index a2cff5a9510b6..f4efdd3b50792 100644 --- a/packages/block-directory/src/store/test/selectors.js +++ b/packages/block-directory/src/store/test/selectors.js @@ -89,9 +89,9 @@ describe( 'selectors', () => { describe( 'getNewBlockTypes', () => { it( 'should retrieve the block types that are installed and in the post content', () => { - getNewBlockTypes.__unstableGetSelect = jest.fn( () => ( { - getBlocks: () => blockList, - } ) ); + getNewBlockTypes.registry = { + select: jest.fn( () => ( { getBlocks: () => blockList } ) ), + }; const state = { blockManagement: { installedBlockTypes: [ @@ -106,9 +106,9 @@ describe( 'selectors', () => { } ); it( 'should return an empty array if no blocks are used', () => { - getNewBlockTypes.__unstableGetSelect = jest.fn( () => ( { - getBlocks: () => [], - } ) ); + getNewBlockTypes.registry = { + select: jest.fn( () => ( { getBlocks: () => [] } ) ), + }; const state = { blockManagement: { installedBlockTypes: [ @@ -124,10 +124,9 @@ describe( 'selectors', () => { describe( 'getUnusedBlockTypes', () => { it( 'should retrieve the block types that are installed but not used', () => { - getUnusedBlockTypes.__unstableGetSelect = jest.fn( () => ( { - getBlocks: () => blockList, - } ) ); - + getUnusedBlockTypes.registry = { + select: jest.fn( () => ( { getBlocks: () => blockList } ) ), + }; const state = { blockManagement: { installedBlockTypes: [ @@ -142,10 +141,9 @@ describe( 'selectors', () => { } ); it( 'should return all block types if no blocks are used', () => { - getUnusedBlockTypes.__unstableGetSelect = jest.fn( () => ( { - getBlocks: () => [], - } ) ); - + getUnusedBlockTypes.registry = { + select: jest.fn( () => ( { getBlocks: () => [] } ) ), + }; const state = { blockManagement: { installedBlockTypes: [ diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index f18e209be9ba8..53abcc48aca48 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -17,7 +17,6 @@ export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useInstanceId } from './hooks/use-instance-id'; export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut'; export { default as useMediaQuery } from './hooks/use-media-query'; -export { default as usePrevious } from './hooks/use-previous'; export { default as useReducedMotion } from './hooks/use-reduced-motion'; export { default as useViewportMatch } from './hooks/use-viewport-match'; export { default as useAsyncList } from './hooks/use-async-list'; diff --git a/packages/data/package.json b/packages/data/package.json index 90f2181dae19b..41679972cf991 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -31,7 +31,6 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/priority-queue": "file:../priority-queue", "@wordpress/redux-routine": "file:../redux-routine", - "@wordpress/stan": "file:../stan", "equivalent-key-map": "^0.2.2", "is-promise": "^4.0.0", "lodash": "^4.17.19", diff --git a/packages/data/src/atomic-store/index.js b/packages/data/src/atomic-store/index.js deleted file mode 100644 index ed6ed7c0d8f82..0000000000000 --- a/packages/data/src/atomic-store/index.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * External dependencies - */ -import { mapValues } from 'lodash'; - -/** - * WordPress dependencies - */ -import { - createDerivedAtom, - createAtomRegistry, - createStoreAtomSelector, -} from '@wordpress/stan'; - -/** - * @typedef {import("../types").WPDataAtomicStoreConfig} WPDataAtomicStoreConfig - */ -/** - * @typedef {import("../types").WPDataStore} WPDataStore - */ -/** - * @template T - * @typedef {import('@wordpress/stan/src/types').WPAtom} WPAtom - */ -/** - * @template T - * @typedef {import('@wordpress/stan/src/types').WPAtomSelector} WPAtomSelector - */ - -/** - * - * @param {string} name Store name. - * @param {WPDataAtomicStoreConfig} config Atomic store config. - * @return {WPDataStore} Store. - */ -export default function createAtomicStore( name, config ) { - return { - name, - instantiate: ( registry ) => { - // Having a dedicated atom registry per store allow us to support - // registry inheritance as we won't be retrieving atoms from the wrong registries. - const atomRegistry = createAtomRegistry(); - - // These are used inside of useSelect when mapSelect can merge selectors - // from different stores and different data registries. - const registryAtomSelectors = mapValues( - config.selectors, - ( atomSelector ) => { - return createStoreAtomSelector( - ( ...args ) => ( listener ) => - atomRegistry.subscribe( - atomSelector( ...args ), - listener - ), - ( ...args ) => () => - atomRegistry.get( atomSelector( ...args ) ), - ( ...args ) => ( value ) => - atomRegistry.set( atomSelector( ...args ), value ) - ); - } - ); - - const selectors = mapValues( - registryAtomSelectors, - ( atomSelector, selectorName ) => { - return ( /** @type {any[]} **/ ...args ) => { - return registry.__internalGetAtomResolver() - ? registry.__internalGetAtomResolver()( - atomSelector( ...args ) - ) - : atomRegistry.get( - // @ts-ignore - config.selectors[ selectorName ]( ...args ) - ); - }; - } - ); - - const actions = mapValues( config.actions, ( atomAction ) => { - return ( /** @type {any[]} **/ ...args ) => { - return atomAction( ...args )( { - get: ( atomCreator ) => atomRegistry.get( atomCreator ), - set: ( atomCreator, value ) => - atomRegistry.set( atomCreator, value ), - } ); - }; - } ); - - return { - __internalIsAtomic: true, - getSelectors: () => selectors, - getActions: () => actions, - - // Subscribing to the root atoms allows us - // To refresh the data when all root selector change. - subscribe: ( listener ) => { - const atom = createDerivedAtom( ( { get } ) => { - config.rootAtoms.forEach( ( subatom ) => - get( subatom ) - ); - } ); - - return atomRegistry.subscribe( atom, listener ); - }, - }; - }, - }; -} diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index dedc7a6c9f084..11cd09d9cdab4 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -1,22 +1,39 @@ +/** + * External dependencies + */ +import { useMemoOne } from 'use-memo-one'; + /** * WordPress dependencies */ +import { createQueue } from '@wordpress/priority-queue'; import { useLayoutEffect, useRef, - useState, - useMemo, useCallback, + useEffect, + useReducer, } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; -import { createDerivedAtom, createAtomRegistry } from '@wordpress/stan'; -import { usePrevious } from '@wordpress/compose'; /** * Internal dependencies */ -import useAsyncMode from '../async-mode-provider/use-async-mode'; import useRegistry from '../registry-provider/use-registry'; +import useAsyncMode from '../async-mode-provider/use-async-mode'; + +/** + * Favor useLayoutEffect to ensure the store subscription callback always has + * the selector from the latest render. If a store update happens between render + * and the effect, this could cause missed/stale updates or inconsistent state. + * + * Fallback to useEffect for server rendered components because currently React + * throws a warning when using useLayoutEffect in that environment. + */ +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +const renderQueue = createQueue(); /** * Custom react hook for retrieving props from registered selectors. @@ -25,14 +42,15 @@ import useRegistry from '../registry-provider/use-registry'; * [rules of hooks](https://reactjs.org/docs/hooks-rules.html). * * @param {Function} _mapSelect Function called on every state change. The - * returned value is exposed to the component - * implementing this hook. The function receives - * the `registry.select` method on the first - * argument and the `registry` on the second - * argument. - * @param {Array} deps If provided, this memoizes the mapSelect so the - * same `mapSelect` is invoked on every state - * change unless the dependencies change. + * returned value is exposed to the component + * implementing this hook. The function receives + * the `registry.select` method on the first + * argument and the `registry` on the second + * argument. + * @param {Array} deps If provided, this memoizes the mapSelect so the + * same `mapSelect` is invoked on every state + * change unless the dependencies change. + * * @example * ```js * import { useSelect } from '@wordpress/data'; @@ -57,51 +75,42 @@ import useRegistry from '../registry-provider/use-registry'; * any price in the state for that currency is retrieved. If the currency prop * doesn't change and other props are passed in that do change, the price will * not change because the dependency is just the currency. + * * @return {Function} A custom react hook. */ export default function useSelect( _mapSelect, deps ) { - const atomRegistry = useMemo( () => createAtomRegistry(), [] ); const mapSelect = useCallback( _mapSelect, deps ); - const previousMapSelect = usePrevious( mapSelect ); - const result = useRef(); const registry = useRegistry(); const isAsync = useAsyncMode(); - const [ , dispatch ] = useState( {} ); - const rerender = () => dispatch( {} ); - const isMountedAndNotUnsubscribing = useRef( true ); - const previousMapError = useRef(); - const shouldSyncCompute = - previousMapSelect !== mapSelect || !! previousMapError.current; - - const atomState = useMemo( () => { - return createDerivedAtom( - ( { get } ) => { - const current = registry.__internalGetAtomResolver(); - registry.__internalSetAtomResolver( get ); - let ret; - try { - ret = mapSelect( registry.select, registry ); - } catch ( error ) { - ret = result.current; - previousMapError.current = error; - } - registry.__internalSetAtomResolver( current ); - return ret; - }, - () => {}, - { isAsync } - )( atomRegistry ); - }, [ isAsync, registry, mapSelect ] ); + // React can sometimes clear the `useMemo` cache. + // We use the cache-stable `useMemoOne` to avoid + // losing queues. + const queueContext = useMemoOne( () => ( { queue: true } ), [ registry ] ); + const [ , forceRender ] = useReducer( ( s ) => s + 1, 0 ); + + const latestMapSelect = useRef(); + const latestIsAsync = useRef( isAsync ); + const latestMapOutput = useRef(); + const latestMapOutputError = useRef(); + const isMountedAndNotUnsubscribing = useRef(); + + let mapOutput; try { - if ( shouldSyncCompute ) { - result.current = atomState.get(); + if ( + latestMapSelect.current !== mapSelect || + latestMapOutputError.current + ) { + mapOutput = mapSelect( registry.select, registry ); + } else { + mapOutput = latestMapOutput.current; } } catch ( error ) { let errorMessage = `An error occurred while running 'mapSelect': ${ error.message }`; - if ( previousMapError.current ) { + + if ( latestMapOutputError.current ) { errorMessage += `\nThe error may be correlated with this previous error:\n`; - errorMessage += `${ previousMapError.current.stack }\n\n`; + errorMessage += `${ latestMapOutputError.current.stack }\n\n`; errorMessage += 'Original stack trace:'; throw new Error( errorMessage ); @@ -110,33 +119,66 @@ export default function useSelect( _mapSelect, deps ) { console.error( errorMessage ); } } - useLayoutEffect( () => { - previousMapError.current = undefined; + + useIsomorphicLayoutEffect( () => { + latestMapSelect.current = mapSelect; + latestMapOutput.current = mapOutput; + latestMapOutputError.current = undefined; isMountedAndNotUnsubscribing.current = true; + + // This has to run after the other ref updates + // to avoid using stale values in the flushed + // callbacks or potentially overwriting a + // changed `latestMapOutput.current`. + if ( latestIsAsync.current !== isAsync ) { + latestIsAsync.current = isAsync; + renderQueue.flush( queueContext ); + } } ); - useLayoutEffect( () => { + useIsomorphicLayoutEffect( () => { const onStoreChange = () => { - if ( - isMountedAndNotUnsubscribing.current && - ! isShallowEqual( atomState.get(), result.current ) - ) { - result.current = atomState.get(); - rerender(); + if ( isMountedAndNotUnsubscribing.current ) { + try { + const newMapOutput = latestMapSelect.current( + registry.select, + registry + ); + if ( + isShallowEqual( latestMapOutput.current, newMapOutput ) + ) { + return; + } + latestMapOutput.current = newMapOutput; + } catch ( error ) { + latestMapOutputError.current = error; + } + forceRender(); } }; - const unsubscribe = atomState.subscribe( () => { + + // catch any possible state changes during mount before the subscription + // could be set. + if ( latestIsAsync.current ) { + renderQueue.add( queueContext, onStoreChange ); + } else { onStoreChange(); - } ); + } - // This is necessary if the value changes during mount. - onStoreChange(); + const unsubscribe = registry.subscribe( () => { + if ( latestIsAsync.current ) { + renderQueue.add( queueContext, onStoreChange ); + } else { + onStoreChange(); + } + } ); return () => { isMountedAndNotUnsubscribing.current = false; unsubscribe(); + renderQueue.flush( queueContext ); }; - }, [ atomState ] ); + }, [ registry ] ); - return result.current; + return mapOutput; } diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 232334af6fb98..78a81ba3d4798 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -47,7 +47,8 @@ describe( 'useSelect', () => { const testInstance = renderer.root; // 2 times expected // - 1 for initial mount - expect( selectSpy ).toHaveBeenCalledTimes( 1 ); + // - 1 for after mount before subscription set. + expect( selectSpy ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); // ensure expected state was rendered @@ -81,7 +82,7 @@ describe( 'useSelect', () => { } ); const testInstance = renderer.root; - expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 0 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); @@ -99,7 +100,7 @@ describe( 'useSelect', () => { ); } ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 0 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); @@ -118,7 +119,7 @@ describe( 'useSelect', () => { ); } ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); @@ -128,17 +129,18 @@ describe( 'useSelect', () => { } ); } ); describe( 'rerenders as expected with various mapSelect return types', () => { - const getComponent = ( mapSelectSpy ) => ( { render } ) => { - const data = useSelect( ( select ) => mapSelectSpy( select ), [ - render, - ] ); + const getComponent = ( mapSelectSpy ) => () => { + const data = useSelect( mapSelectSpy, [] ); return
; }; - let TestComponent; + let subscribedSpy, TestComponent; const mapSelectSpy = jest.fn( ( select ) => select( 'testStore' ).testSelector() ); const selectorSpy = jest.fn(); + const subscribeCallback = ( subscription ) => { + subscribedSpy = subscription; + }; beforeEach( () => { registry.registerStore( 'testStore', { @@ -147,6 +149,7 @@ describe( 'useSelect', () => { testSelector: selectorSpy, }, } ); + registry.subscribe = subscribeCallback; TestComponent = getComponent( mapSelectSpy ); } ); afterEach( () => { @@ -177,7 +180,7 @@ describe( 'useSelect', () => { act( () => { renderer = TestRenderer.create( - + ); } ); @@ -191,16 +194,12 @@ describe( 'useSelect', () => { // subscription which should in turn trigger a re-render. act( () => { selectorSpy.mockReturnValue( valueB ); - renderer.update( - - - - ); + subscribedSpy(); } ); expect( testInstance.findByType( 'div' ).props.data ).toEqual( valueB ); - expect( mapSelectSpy ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectSpy ).toHaveBeenCalledTimes( 3 ); } ); } ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index f9c67dd243116..26f521c99059b 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -62,7 +62,10 @@ describe( 'withSelect', () => { ); } ); const testInstance = testRenderer.root; - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // Expected two times: + // - Once on initial render. + // - Once on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); // Wrapper is the enhanced component. Find props on the rendered child. @@ -122,7 +125,10 @@ describe( 'withSelect', () => { const testInstance = testRenderer.root; expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( mapDispatchToProps ).toHaveBeenCalledTimes( 1 ); // Simulate a click on the button @@ -131,8 +137,16 @@ describe( 'withSelect', () => { } ); expect( testInstance.findByType( 'button' ).props.children ).toBe( 1 ); + // 2 times = + // 1. Initial mount + // 2. When click handler is called expect( mapDispatchToProps ).toHaveBeenCalledTimes( 2 ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + // 4 times + // - 1 on initial render + // - 1 on effect before subscription set. + // - 1 on click triggering subscription firing. + // - 1 on rerender. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); // verifies component only renders twice. expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -209,7 +223,7 @@ describe( 'withSelect', () => { expect( testInstance.findByType( 'div' ).props.children ).toBe( 2 ); // Expected 3 times because: // - 1 on initial render - // - 1 on atom subscription (resolve). + // - 1 on effect before subscription set. // - 1 for the rerender because of the mapOutput change detected. expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( renderSpy ).toHaveBeenCalledTimes( 2 ); @@ -221,6 +235,10 @@ describe( 'withSelect', () => { } ); testInstance = testRenderer.root; expect( testInstance.findByType( 'div' ).props.children ).toBe( 4 ); + // Expected an additional 3 times because of the unmount and remount: + // - 1 on initial render + // - 1 on effect before subscription set. + // - once for the rerender because of the mapOutput change detected. expect( mapSelectToProps ).toHaveBeenCalledTimes( 6 ); expect( renderSpy ).toHaveBeenCalledTimes( 4 ); } ); @@ -264,7 +282,10 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); act( () => { @@ -276,7 +297,7 @@ describe( 'withSelect', () => { } ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 10 ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -309,7 +330,10 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); act( () => { @@ -320,7 +344,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -355,12 +379,15 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); registry.dispatch( 'demo' ).update(); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -389,7 +416,10 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); act( () => { @@ -400,7 +430,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -428,12 +458,15 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); store.dispatch( { type: 'dummy' } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -473,7 +506,10 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( @@ -491,7 +527,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( JSON.parse( testInstance.findByType( 'div' ).props.children ) @@ -539,7 +575,10 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'Unknown' @@ -553,7 +592,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'OK' ); @@ -565,7 +604,7 @@ describe( 'withSelect', () => { ); } ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 3 ); expect( testInstance.findByType( 'div' ).props.children ).toBe( 'Unknown' @@ -615,8 +654,11 @@ describe( 'withSelect', () => { ); } ); - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 1 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -624,8 +666,12 @@ describe( 'withSelect', () => { registry.dispatch( 'childRender' ).toggleRender(); } ); - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 1 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 3 ); + // 3 times because + // - 1 on initial render + // - 1 on effect before subscription set. + // - 1 child subscription fires. + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -663,7 +709,10 @@ describe( 'withSelect', () => { } ); const testInstance = testRenderer.root; - expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); + // 2 times: + // - 1 on initial render + // - 1 on effect before subscription set. + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( testInstance.findByType( 'div' ).props ).toEqual( { @@ -685,8 +734,12 @@ describe( 'withSelect', () => { ); } ); - - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + // 4 times: + // - 1 on initial render + // - 1 on effect before subscription set. + // - 1 on re-render + // - 1 on effect before new subscription set (because registry has changed) + expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( testInstance.findByType( 'div' ).props ).toEqual( { diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index 73dcc3b5d3dd1..400f4ffe15c0a 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -36,11 +36,11 @@ * @return {Function} Registry selector that can be registered with a store. */ export function createRegistrySelector( registrySelector ) { - // create a selector function that is bound to the registry referenced by `selector.__unstableGetSelect` + // create a selector function that is bound to the registry referenced by `selector.registry` // and that has the same API as a regular selector. Binding it in such a way makes it // possible to call the selector directly from another selector. const selector = ( ...args ) => - registrySelector( selector.__unstableGetSelect )( ...args ); + registrySelector( selector.registry.select )( ...args ); /** * Flag indicating that the selector is a registry selector that needs the correct registry diff --git a/packages/data/src/index.js b/packages/data/src/index.js index 67552b1b487e7..21d8bb6337c88 100644 --- a/packages/data/src/index.js +++ b/packages/data/src/index.js @@ -26,7 +26,6 @@ export { createRegistry } from './registry'; export { createRegistrySelector, createRegistryControl } from './factory'; export { controls } from './controls'; export { default as createReduxStore } from './redux-store'; -export { default as __experimentalCreateAtomicStore } from './atomic-store'; /** * Object of available plugins to use with a registry. diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index e0894a8fd7d5d..55117182ba78c 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -84,6 +84,7 @@ export default function createReduxStore( key, options ) { const store = instantiateReduxStore( key, options, registry ); const resolversCache = createResolversCache(); + let resolvers; const actions = mapActions( { ...metadataActions, @@ -91,123 +92,34 @@ export default function createReduxStore( key, options ) { }, store ); - - // Inject registry into selectors - // It is important that this injection happens first because __unstableGetSelect - // is injected using a mutation of the original selector function. - const selectorsWithRegistry = mapValues( - options.selectors, - ( selector ) => { - if ( selector.isRegistrySelector ) { - selector.__unstableGetSelect = registry.select; - } - return selector; - } - ); - - // Inject state into selectors - const injectState = ( getState, selector ) => { - const mappedSelector = ( ...args ) => - selector( getState(), ...args ); - mappedSelector.__unstableRegistrySelector = - selector.__unstableRegistrySelector; - return mappedSelector; - }; - const selectorsWithState = { - ...mapValues( metadataSelectors, ( selector ) => - injectState( - () => store.__unstableOriginalGetState().metadata, - selector - ) - ), - ...mapValues( selectorsWithRegistry, ( selector ) => - injectState( - () => store.__unstableOriginalGetState().root, - selector - ) - ), - }; - - // Normalize resolvers - const resolvers = mapValues( options.resolvers, ( resolver ) => { - if ( resolver.fulfill ) { - return resolver; - } - - return { - ...resolver, // copy the enumerable properties of the resolver function - fulfill: resolver, // add the fulfill method - }; - } ); - - // Inject resolvers fullfilment call into selectors. - const selectors = mapValues( - selectorsWithState, - ( selector, selectorName ) => { - const resolver = resolvers[ selectorName ]; - if ( ! resolver ) { - selector.hasResolver = false; - return selector; - } - - async function fulfillSelector( args ) { - const state = store.getState(); - if ( - resolversCache.isRunning( selectorName, args ) || - ( typeof resolver.isFulfilled === 'function' && - resolver.isFulfilled( state, ...args ) ) - ) { - return; - } - - const { metadata } = store.__unstableOriginalGetState(); - - if ( - metadataSelectors.hasStartedResolution( - metadata, - selectorName, - args - ) - ) { - return; + let selectors = mapSelectors( + { + ...mapValues( + metadataSelectors, + ( selector ) => ( state, ...args ) => + selector( state.metadata, ...args ) + ), + ...mapValues( options.selectors, ( selector ) => { + if ( selector.isRegistrySelector ) { + selector.registry = registry; } - resolversCache.markAsRunning( selectorName, args ); - - setTimeout( async () => { - resolversCache.clear( selectorName, args ); - store.dispatch( - metadataActions.startResolution( - selectorName, - args - ) - ); - await fulfillResolver( - store, - resolvers, - selectorName, - ...args - ); - store.dispatch( - metadataActions.finishResolution( - selectorName, - args - ) - ); - } ); - } - - const mappedSelector = ( ...args ) => { - fulfillSelector( args ); - return selector( ...args ); - }; - mappedSelector.__unstableRegistrySelector = - selector.__unstableRegistrySelector; - mappedSelector.hasResolver = true; - - return mappedSelector; - } + return ( state, ...args ) => + selector( state.root, ...args ); + } ), + }, + store ); + if ( options.resolvers ) { + const result = mapResolvers( + options.resolvers, + selectors, + store, + resolversCache + ); + resolvers = result.resolvers; + selectors = result.selectors; + } const getSelectors = () => selectors; const getActions = () => actions; @@ -304,6 +216,41 @@ function instantiateReduxStore( key, options, registry ) { ); } +/** + * Maps selectors to a store. + * + * @param {Object} selectors Selectors to register. Keys will be used as the + * public facing API. Selectors will get passed the + * state as first argument. + * @param {Object} store The store to which the selectors should be mapped. + * @return {Object} Selectors mapped to the provided store. + */ +function mapSelectors( selectors, store ) { + const createStateSelector = ( registrySelector ) => { + const selector = function runSelector() { + // This function is an optimized implementation of: + // + // selector( store.getState(), ...arguments ) + // + // Where the above would incur an `Array#concat` in its application, + // the logic here instead efficiently constructs an arguments array via + // direct assignment. + const argsLength = arguments.length; + const args = new Array( argsLength + 1 ); + args[ 0 ] = store.__unstableOriginalGetState(); + for ( let i = 0; i < argsLength; i++ ) { + args[ i + 1 ] = arguments[ i ]; + } + + return registrySelector( ...args ); + }; + selector.hasResolver = false; + return selector; + }; + + return mapValues( selectors, createStateSelector ); +} + /** * Maps actions to dispatch from a given store. * @@ -319,6 +266,93 @@ function mapActions( actions, store ) { return mapValues( actions, createBoundAction ); } +/** + * Returns resolvers with matched selectors for a given namespace. + * Resolvers are side effects invoked once per argument set of a given selector call, + * used in ensuring that the data needs for the selector are satisfied. + * + * @param {Object} resolvers Resolvers to register. + * @param {Object} selectors The current selectors to be modified. + * @param {Object} store The redux store to which the resolvers should be mapped. + * @param {Object} resolversCache Resolvers Cache. + */ +function mapResolvers( resolvers, selectors, store, resolversCache ) { + // The `resolver` can be either a function that does the resolution, or, in more advanced + // cases, an object with a `fullfill` method and other optional methods like `isFulfilled`. + // Here we normalize the `resolver` function to an object with `fulfill` method. + const mappedResolvers = mapValues( resolvers, ( resolver ) => { + if ( resolver.fulfill ) { + return resolver; + } + + return { + ...resolver, // copy the enumerable properties of the resolver function + fulfill: resolver, // add the fulfill method + }; + } ); + + const mapSelector = ( selector, selectorName ) => { + const resolver = resolvers[ selectorName ]; + if ( ! resolver ) { + selector.hasResolver = false; + return selector; + } + + const selectorResolver = ( ...args ) => { + async function fulfillSelector() { + const state = store.getState(); + if ( + resolversCache.isRunning( selectorName, args ) || + ( typeof resolver.isFulfilled === 'function' && + resolver.isFulfilled( state, ...args ) ) + ) { + return; + } + + const { metadata } = store.__unstableOriginalGetState(); + + if ( + metadataSelectors.hasStartedResolution( + metadata, + selectorName, + args + ) + ) { + return; + } + + resolversCache.markAsRunning( selectorName, args ); + + setTimeout( async () => { + resolversCache.clear( selectorName, args ); + store.dispatch( + metadataActions.startResolution( selectorName, args ) + ); + await fulfillResolver( + store, + mappedResolvers, + selectorName, + ...args + ); + store.dispatch( + metadataActions.finishResolution( selectorName, args ) + ); + } ); + } + + fulfillSelector( ...args ); + return selector( ...args ); + }; + selectorResolver.hasResolver = true; + return selectorResolver; + }; + + return { + resolvers: mappedResolvers, + selectors: mapValues( selectors, mapSelector ), + }; +} + /** * Calls a resolver given arguments * diff --git a/packages/data/src/redux-store/test/index.js b/packages/data/src/redux-store/test/index.js index 73c907eaf341c..0198f5972004b 100644 --- a/packages/data/src/redux-store/test/index.js +++ b/packages/data/src/redux-store/test/index.js @@ -1,21 +1,10 @@ -/** - * WordPress dependencies - */ -import { createAtomRegistry, createDerivedAtom } from '@wordpress/stan'; - /** * Internal dependencies */ import { createRegistry } from '../../registry'; -import { createRegistryControl, createRegistrySelector } from '../../factory'; +import { createRegistryControl } from '../../factory'; jest.useFakeTimers(); -async function flushImmediatesAndTicks( count = 1 ) { - for ( let i = 0; i < count; i++ ) { - await jest.runAllTicks(); - await jest.runAllImmediates(); - } -} describe( 'controls', () => { let registry; @@ -244,162 +233,4 @@ describe( 'controls', () => { ); } ); } ); - - describe( 'atomSelectors', () => { - const createUseSelectAtom = ( mapSelectToProps ) => { - return createDerivedAtom( ( { get } ) => { - const current = registry.__internalGetAtomResolver(); - registry.__internalSetAtomResolver( get ); - const ret = mapSelectToProps( registry.select ); - registry.__internalSetAtomResolver( current ); - return ret; - } ); - }; - - beforeEach( () => { - registry = createRegistry(); - const action1 = ( value ) => ( { type: 'set', value } ); - registry.registerStore( 'store1', { - reducer: ( state = 'default', action ) => { - if ( action.type === 'set' ) { - return action.value; - } - return state; - }, - actions: { - set: action1, - }, - selectors: { - getValue( state ) { - return state; - }, - }, - } ); - } ); - - it( 'should subscribe to atom selectors', async () => { - const atomRegistry = createAtomRegistry(); - const atom = createUseSelectAtom( ( select ) => { - return { - value: select( 'store1' ).getValue(), - }; - } ); - const unsubscribe = atomRegistry.subscribe( atom, () => {} ); - await flushImmediatesAndTicks(); - expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); - registry.dispatch( 'store1' ).set( 'new' ); - await flushImmediatesAndTicks(); - expect( atomRegistry.get( atom ).value ).toEqual( 'new' ); - unsubscribe(); - } ); - - it( 'should subscribe to not subscribe to unrelated stores', async () => { - const action1 = ( value ) => ( { type: 'set', value } ); - registry.registerStore( 'store2', { - reducer: ( state = 'default', action ) => { - if ( action.type === 'set' ) { - return action.value; - } - return state; - }, - actions: { - set: action1, - }, - selectors: { - getValue( state ) { - return state; - }, - }, - } ); - - const atom = createUseSelectAtom( ( select ) => { - return { - value: select( 'store1' ).getValue(), - }; - } ); - const atomRegistry = createAtomRegistry(); - const update = jest.fn(); - const unsubscribe = atomRegistry.subscribe( atom, update ); - await flushImmediatesAndTicks( 2 ); - expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); - // Reset the call that happens for initialization. - update.mockClear(); - registry.dispatch( 'store2' ).set( 'new' ); - await flushImmediatesAndTicks( 2 ); - expect( update ).not.toHaveBeenCalled(); - unsubscribe(); - } ); - - it( 'should subscribe to sub stores for registry selectors', async () => { - registry.registerStore( 'store2', { - reducer: () => 'none', - actions: {}, - selectors: { - getSubStoreValue: createRegistrySelector( - ( select ) => () => { - return select( 'store1' ).getValue(); - } - ), - }, - } ); - - const atomRegistry = createAtomRegistry(); - const atom = createUseSelectAtom( ( select ) => { - return { - value: select( 'store2' ).getSubStoreValue(), - }; - } ); - - const unsubscribe = atomRegistry.subscribe( atom, () => {} ); - await flushImmediatesAndTicks( 10 ); - expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); - registry.dispatch( 'store1' ).set( 'new' ); - await flushImmediatesAndTicks( 10 ); - expect( atomRegistry.get( atom ).value ).toEqual( 'new' ); - unsubscribe(); - } ); - - it( 'should subscribe to nested sub stores for registry selectors', async () => { - registry.registerStore( 'store2', { - reducer: () => 'none', - actions: {}, - selectors: { - getSubStoreValue: createRegistrySelector( - ( select ) => () => { - return select( 'store1' ).getValue(); - } - ), - }, - } ); - - const getSubStoreValue = createRegistrySelector( - ( select ) => () => { - return select( 'store2' ).getSubStoreValue(); - } - ); - registry.registerStore( 'store3', { - reducer: () => 'none', - actions: {}, - selectors: { - getSubStoreValue, - getAdjacentSelectValue: () => getSubStoreValue(), - }, - } ); - - const atomRegistry = createAtomRegistry(); - const atom = createUseSelectAtom( ( select ) => { - return { - value: select( 'store3' ).getSubStoreValue(), - }; - } ); - - const unsubscribe = atomRegistry.subscribe( atom, () => {} ); - await flushImmediatesAndTicks( 4 ); - expect( atomRegistry.get( atom ).value ).toEqual( 'default' ); - registry.dispatch( 'store1' ).set( 'new' ); - await flushImmediatesAndTicks( 4 ); - expect( atomRegistry.get( atom ).value ).toEqual( 'new' ); - unsubscribe(); - } ); - } ); } ); diff --git a/packages/data/src/registry.js b/packages/data/src/registry.js index 0585011160ca9..674227f131409 100644 --- a/packages/data/src/registry.js +++ b/packages/data/src/registry.js @@ -4,11 +4,6 @@ import { omit, without, mapValues, isObject } from 'lodash'; import memize from 'memize'; -/** - * WordPress dependencies - */ -import { createStoreAtom } from '@wordpress/stan'; - /** * Internal dependencies */ @@ -54,7 +49,6 @@ import createCoreDataStore from './store'; */ export function createRegistry( storeConfigs = {}, parent = null ) { const stores = {}; - const storesAtoms = {}; let listeners = []; /** @@ -79,12 +73,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { }; }; - /** - * This is used to track the current atom resolver - * and inject it into registry selectors. - */ - let currentAtomResolver; - /** * Calls a selector given the current state and extra arguments. * @@ -97,25 +85,12 @@ export function createRegistry( storeConfigs = {}, parent = null ) { const storeName = isObject( storeNameOrDefinition ) ? storeNameOrDefinition.name : storeNameOrDefinition; - const store = stores[ storeName ]; if ( store ) { - // If it's not an atomic store subscribe to the store. - if ( - ! store.__internalIsAtomic && - registry.__internalGetAtomResolver() - ) { - registry.__internalGetAtomResolver()( - registry.__internalGetAtomForStore( storeName ) - ); - } - return store.getSelectors(); } - if ( parent ) { - return parent.select( storeName ); - } + return parent && parent.select( storeName ); } const getResolveSelectors = memize( @@ -226,12 +201,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { throw new TypeError( 'config.subscribe must be a function' ); } stores[ key ] = config; - storesAtoms[ key ] = createStoreAtom( - config.subscribe, - () => null, - () => {}, - { id: key } - ); config.subscribe( globalListener ); } @@ -244,15 +213,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { registerGenericStore( store.name, store.instantiate( registry ) ); } - function __internalGetAtomForStore( key ) { - const atom = storesAtoms[ key ]; - if ( atom ) { - return atom; - } - - return parent.__internalGetAtomForStore( key ); - } - let registry = { registerGenericStore, stores, @@ -263,16 +223,6 @@ export function createRegistry( storeConfigs = {}, parent = null ) { dispatch, use, register, - __internalGetAtomForStore, - __internalGetAtomResolver() { - return currentAtomResolver; - }, - __internalSetAtomResolver( resolver ) { - if ( parent ) { - parent.__internalSetAtomResolver( resolver ); - } - currentAtomResolver = resolver; - }, }; /** diff --git a/packages/data/src/types.d.ts b/packages/data/src/types.d.ts new file mode 100644 index 0000000000000..5c006762b0e21 --- /dev/null +++ b/packages/data/src/types.d.ts @@ -0,0 +1,32 @@ +export type WPDataFunctionOrGeneratorArray = Array< Function | Generator >; +export type WPDataFunctionArray = Array< Function >; + +export interface WPDataAttachedStore { + getSelectors: () => WPDataFunctionArray, + getActions: () => WPDataFunctionArray, + subscribe: (listener: () => void) => (() => void) +}; + +export interface WPDataStore { + /** + * Store Name + */ + name: string, + + /** + * Store configuration object. + */ + instantiate: (registry: WPDataRegistry) => WPDataAttachedStore, +}; + +export interface WPDataReduxStoreConfig { + reducer: ( state: any, action: any ) => any, + actions?: WPDataFunctionOrGeneratorArray, + resolvers?: WPDataFunctionOrGeneratorArray, + selectors?: WPDataFunctionArray, + controls?: WPDataFunctionArray, +} + +export interface WPDataRegistry { + register: ( store: WPDataStore ) => void, +} \ No newline at end of file diff --git a/packages/data/src/types.ts b/packages/data/src/types.ts deleted file mode 100644 index 7663d9c1dca48..0000000000000 --- a/packages/data/src/types.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * WordPress dependencies - */ -import type { - WPAtom, - WPAtomSelector, - WPAtomResolver, - WPAtomUpdater, -} from '@wordpress/stan'; - -export type WPDataFunctionOrGeneratorArray = { - [ key: string ]: Function | Generator; -}; -export type WPDataFunctionArray = { [ key: string ]: Function }; - -export interface WPDataAttachedStore { - getSelectors: () => WPDataFunctionArray; - getActions: () => WPDataFunctionArray; - subscribe: ( listener: () => void ) => () => void; - __internalIsAtomic?: boolean; -} - -export interface WPDataStore { - /** - * Store Name - */ - name: string; - - /** - * Store configuration object. - */ - instantiate: ( registry: WPDataRegistry ) => WPDataAttachedStore; -} - -export interface WPDataReduxStoreConfig { - reducer: ( state: any, action: any ) => any; - actions?: WPDataFunctionOrGeneratorArray; - resolvers?: WPDataFunctionOrGeneratorArray; - selectors?: WPDataFunctionArray; - controls?: WPDataFunctionArray; -} - -export type WPDataAtomicStoreAction< T > = ( - ...args: any[] -) => ( props: { get: WPAtomResolver< T >; set: WPAtomUpdater< T > } ) => void; - -export interface WPDataAtomicStoreConfig { - rootAtoms: Array< WPAtom< any > >; - actions?: { [ key: string ]: WPDataAtomicStoreAction< any > }; - selectors?: { [ key: string ]: (...args:any[]) => WPAtomSelector }; -} - -export interface WPDataRegistry { - /** - * Registers a store. - */ - register: ( store: WPDataStore ) => void; - - /** - * For registry selectors we need to be able to inject the atom resolver. - * This setter/getter allows us to so. - */ - __internalGetAtomResolver: () => WPAtomResolver< any >; - - /** - * Sets the current atom resolver in the registry. - */ - __internalSetAtomResolver: ( resolver: WPAtomResolver< any > ) => void; - - /** - * Retrieve or creates an atom per store. - * This atom allows selectors to refresh when any change happen on that store. - */ - __internalGetAtomForStore: ( storeName: string ) => WPAtom< any >; -} diff --git a/packages/data/tsconfig.json b/packages/data/tsconfig.json deleted file mode 100644 index 75b18c62f6ce5..0000000000000 --- a/packages/data/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "references": [ - { "path": "../stan" }, - ], - "include": [ - "src/types.ts", - "src/atomic-store/*", - ], - "exclude": [ - "src/**/test" - ] -} diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index 7748c01e00a75..21854bb50c006 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -1,9 +1,5 @@ const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ - '@wordpress/icons', - '@wordpress/interface', - '@wordpress/stan', -]; +const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; /** * Default request to global transformation diff --git a/packages/e2e-tests/specs/editor/various/typewriter.test.js b/packages/e2e-tests/specs/editor/various/typewriter.test.js index 464f265a609fe..d1983e465e785 100644 --- a/packages/e2e-tests/specs/editor/various/typewriter.test.js +++ b/packages/e2e-tests/specs/editor/various/typewriter.test.js @@ -3,14 +3,6 @@ */ import { createNewPost } from '@wordpress/e2e-test-utils'; -async function waitForTheTypewriterEffectToTrigger() { - // Wait for the browser to be idle - // the typewriter effect uses the same delay. - await page.waitForFunction( () => { - return new Promise( window.requestAnimationFrame ); - } ); -} - describe( 'TypeWriter', () => { beforeEach( async () => { await createNewPost(); @@ -55,8 +47,6 @@ describe( 'TypeWriter', () => { // Now the scroll position should be maintained. await page.keyboard.press( 'Enter' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); // Type until the text wraps. @@ -73,16 +63,12 @@ describe( 'TypeWriter', () => { await page.keyboard.type( 'a' ); } - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); // Pressing backspace will reposition the caret to the previous line. // Scroll position should be adjusted again. await page.keyboard.press( 'Backspace' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); // Should reset scroll position to maintain. @@ -95,8 +81,6 @@ describe( 'TypeWriter', () => { // Should be scrolled to new position. await page.keyboard.press( 'Enter' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( positionAfterArrowUp ) ).toBeLessThanOrEqual( BUFFER ); @@ -127,8 +111,6 @@ describe( 'TypeWriter', () => { // Should maintain scroll position. await page.keyboard.press( 'Enter' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( BUFFER ); @@ -150,8 +132,6 @@ describe( 'TypeWriter', () => { // Should maintain scroll position. await page.keyboard.press( 'Enter' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( BUFFER ); @@ -206,8 +186,6 @@ describe( 'TypeWriter', () => { // Should maintain new caret position. await page.keyboard.press( 'Enter' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( newBottomPosition ) ).toBeLessThanOrEqual( BUFFER ); @@ -238,8 +216,6 @@ describe( 'TypeWriter', () => { // Should maintain new caret position. await page.keyboard.press( 'Enter' ); - await waitForTheTypewriterEffectToTrigger(); - expect( await getDiff( newTopPosition ) ).toBeLessThanOrEqual( BUFFER ); } ); } ); diff --git a/packages/edit-navigation/src/store/test/selectors.js b/packages/edit-navigation/src/store/test/selectors.js index c058b6b829b08..450283f2661d2 100644 --- a/packages/edit-navigation/src/store/test/selectors.js +++ b/packages/edit-navigation/src/store/test/selectors.js @@ -12,64 +12,81 @@ describe( 'getNavigationPostForMenu', () => { it( 'gets navigation post for menu', () => { const getEditedEntityRecord = jest.fn( () => 'record' ); const hasFinishedResolution = jest.fn( () => true ); - const __unstableGetSelect = jest.fn( () => ( { - getEditedEntityRecord, - hasFinishedResolution, - } ) ); + const registry = { + select: jest.fn( () => ( { + getEditedEntityRecord, + hasFinishedResolution, + } ) ), + }; const menuId = 123; - getNavigationPostForMenu.__unstableGetSelect = __unstableGetSelect; - hasResolvedNavigationPost.__unstableGetSelect = __unstableGetSelect; + const defaultRegistry = getNavigationPostForMenu.registry; + getNavigationPostForMenu.registry = registry; + hasResolvedNavigationPost.registry = registry; expect( getNavigationPostForMenu( 'state', menuId ) ).toBe( 'record' ); - expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); + expect( registry.select ).toHaveBeenCalledWith( 'core' ); expect( getEditedEntityRecord ).toHaveBeenCalledWith( KIND, POST_TYPE, buildNavigationPostId( menuId ) ); + + getNavigationPostForMenu.registry = defaultRegistry; + hasResolvedNavigationPost.registry = defaultRegistry; } ); it( 'returns null if has not resolved navigation post yet', () => { const getEditedEntityRecord = jest.fn( () => 'record' ); const hasFinishedResolution = jest.fn( () => false ); - const __unstableGetSelect = jest.fn( () => ( { - getEditedEntityRecord, - hasFinishedResolution, - } ) ); + const registry = { + select: jest.fn( () => ( { + getEditedEntityRecord, + hasFinishedResolution, + } ) ), + }; const menuId = 123; - getNavigationPostForMenu.__unstableGetSelect = __unstableGetSelect; - hasResolvedNavigationPost.__unstableGetSelect = __unstableGetSelect; + const defaultRegistry = getNavigationPostForMenu.registry; + getNavigationPostForMenu.registry = registry; + hasResolvedNavigationPost.registry = registry; expect( getNavigationPostForMenu( 'state', menuId ) ).toBe( null ); - expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); + expect( registry.select ).toHaveBeenCalledWith( 'core' ); expect( getEditedEntityRecord ).not.toHaveBeenCalled(); + + getNavigationPostForMenu.registry = defaultRegistry; + hasResolvedNavigationPost.registry = defaultRegistry; } ); } ); describe( 'hasResolvedNavigationPost', () => { it( 'returns if it has resolved navigation post yet', () => { const hasFinishedResolution = jest.fn( () => true ); - const __unstableGetSelect = jest.fn( () => ( { - hasFinishedResolution, - } ) ); + const registry = { + select: jest.fn( () => ( { + hasFinishedResolution, + } ) ), + }; const menuId = 123; - hasResolvedNavigationPost.__unstableGetSelect = __unstableGetSelect; + const defaultRegistry = getNavigationPostForMenu.registry; + hasResolvedNavigationPost.registry = registry; expect( hasResolvedNavigationPost( 'state', menuId ) ).toBe( true ); - expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); + expect( registry.select ).toHaveBeenCalledWith( 'core' ); expect( hasFinishedResolution ).toHaveBeenCalledWith( 'getEntityRecord', [ KIND, POST_TYPE, buildNavigationPostId( menuId ) ] ); + + hasResolvedNavigationPost.registry = defaultRegistry; } ); } ); @@ -77,9 +94,11 @@ describe( 'getMenuItemForClientId', () => { it( 'gets menu item for client id', () => { const getMenuItem = jest.fn( () => 'menuItem' ); - const __unstableGetSelect = jest.fn( () => ( { - getMenuItem, - } ) ); + const registry = { + select: jest.fn( () => ( { + getMenuItem, + } ) ), + }; const state = { mapping: { @@ -89,13 +108,16 @@ describe( 'getMenuItemForClientId', () => { }, }; - getMenuItemForClientId.__unstableGetSelect = __unstableGetSelect; + const defaultRegistry = getMenuItemForClientId.registry; + getMenuItemForClientId.registry = registry; expect( getMenuItemForClientId( state, 'postId', 'clientId' ) ).toBe( 'menuItem' ); - expect( __unstableGetSelect ).toHaveBeenCalledWith( 'core' ); + expect( registry.select ).toHaveBeenCalledWith( 'core' ); expect( getMenuItem ).toHaveBeenCalledWith( '123' ); + + getMenuItemForClientId.registry = defaultRegistry; } ); } ); diff --git a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js index ee6c73aead797..853d267243189 100644 --- a/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js +++ b/packages/edit-post/src/components/editor-initialization/test/listener-hooks.js @@ -35,20 +35,17 @@ describe( 'listener hook tests', () => { getActiveGeneralSidebarName: jest.fn(), }, }; - let atomResolver; + let subscribeTrigger; const registry = { - __internalGetAtomRegistry: () => ( {} ), - __internalGetAtomResolver: () => atomResolver, - __internalSetAtomResolver: ( resolver ) => { - atomResolver = resolver; - }, select: jest .fn() .mockImplementation( ( storeName ) => mockStores[ storeName ] ), dispatch: jest .fn() .mockImplementation( ( storeName ) => mockStores[ storeName ] ), - subscribe: () => {}, + subscribe: ( subscription ) => { + subscribeTrigger = subscription; + }, }; const setMockReturnValue = ( store, functionName, value ) => { mockStores[ store ][ functionName ] = jest @@ -77,6 +74,7 @@ describe( 'listener hook tests', () => { mock.mockClear(); } ); } ); + subscribeTrigger = undefined; } ); describe( 'useBlockSelectionListener', () => { it( 'does nothing when editor sidebar is not open', () => { @@ -159,6 +157,10 @@ describe( 'listener hook tests', () => { renderComponent( useUpdatePostLinkListener, 20, renderer ); } ); expect( mockSelector ).toHaveBeenCalledTimes( 1 ); + act( () => { + subscribeTrigger(); + } ); + expect( mockSelector ).toHaveBeenCalledTimes( 1 ); } ); it( 'only updates the permalink when it changes', () => { setMockReturnValue( 'core/editor', 'getCurrentPost', { @@ -167,7 +169,26 @@ describe( 'listener hook tests', () => { act( () => { renderComponent( useUpdatePostLinkListener, 10 ); } ); + act( () => { + subscribeTrigger(); + } ); expect( setAttribute ).toHaveBeenCalledTimes( 1 ); } ); + it( 'updates the permalink when it changes', () => { + setMockReturnValue( 'core/editor', 'getCurrentPost', { + link: 'foo', + } ); + act( () => { + renderComponent( useUpdatePostLinkListener, 10 ); + } ); + setMockReturnValue( 'core/editor', 'getCurrentPost', { + link: 'bar', + } ); + act( () => { + subscribeTrigger(); + } ); + expect( setAttribute ).toHaveBeenCalledTimes( 2 ); + expect( setAttribute ).toHaveBeenCalledWith( 'href', 'bar' ); + } ); } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index c9a26c0a24c19..7a187e99d7831 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -17,9 +17,9 @@ import { describe( 'selectors', () => { const canUser = jest.fn( () => true ); - getCanUserCreateMedia.__unstableGetSelect = jest.fn( () => ( { - canUser, - } ) ); + getCanUserCreateMedia.registry = { + select: jest.fn( () => ( { canUser } ) ), + }; describe( 'isFeatureActive', () => { it( 'is tolerant to an undefined features preference', () => { @@ -70,7 +70,7 @@ describe( 'selectors', () => { it( "selects `canUser( 'create', 'media' )` from the core store", () => { expect( getCanUserCreateMedia() ).toBe( true ); expect( - getCanUserCreateMedia.__unstableGetSelect + getCanUserCreateMedia.registry.select ).toHaveBeenCalledWith( 'core' ); expect( canUser ).toHaveBeenCalledWith( 'create', 'media' ); } ); diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index d98e32ee00650..493931b5003e3 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -61,7 +61,7 @@ export default function PostTitle() { isFocusMode: focusMode, hasFixedToolbar: _hasFixedToolbar, }; - }, [] ); + } ); useEffect( () => { const { ownerDocument } = ref.current; diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index b369c28f9a318..51eba62c14505 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -54,7 +54,6 @@ export function* setupEditor( post, edits, template ) { } yield resetPost( post ); - yield { type: 'SETUP_EDITOR', post, @@ -65,7 +64,6 @@ export function* setupEditor( post, edits, template ) { __unstableShouldCreateUndoLevel: false, } ); yield setupEditorState( post ); - if ( edits && Object.keys( edits ).some( diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index d7ef6c5de3293..fd4a84ecbdbdc 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -116,7 +116,7 @@ selectorNames.forEach( ( name ) => { selectorNames.forEach( ( otherName ) => { if ( _selectors[ otherName ].isRegistrySelector ) { - _selectors[ otherName ].__unstableGetSelect = select; + _selectors[ otherName ].registry = { select }; } } ); @@ -125,8 +125,9 @@ selectorNames.forEach( ( name ) => { selectors[ name ].isRegistrySelector = _selectors[ name ].isRegistrySelector; if ( selectors[ name ].isRegistrySelector ) { - selectors[ name ].__unstableGetSelect = - _selectors[ name ].__unstableGetSelect; + selectors[ name ].registry = { + select: () => _selectors[ name ].registry.select(), + }; } } ); const { diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index 162dff23cbd72..644ab39c61670 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -27,7 +27,6 @@ "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", "@wordpress/keycodes": "file:../keycodes", - "@wordpress/stan": "file:../stan", "lodash": "^4.17.19", "rememo": "^3.0.0" }, diff --git a/packages/keyboard-shortcuts/src/store/actions.js b/packages/keyboard-shortcuts/src/store/actions.js index 865ab9a3a796a..b4cb625287d8f 100644 --- a/packages/keyboard-shortcuts/src/store/actions.js +++ b/packages/keyboard-shortcuts/src/store/actions.js @@ -1,13 +1,3 @@ -/** - * External dependencies - */ -import { omit } from 'lodash'; - -/** - * Internal dependencies - */ -import { shortcutsByNameAtom, shortcutNamesAtom } from './atoms'; - /** @typedef {import('@wordpress/keycodes').WPKeycodeModifier} WPKeycodeModifier */ /** @@ -34,40 +24,37 @@ import { shortcutsByNameAtom, shortcutNamesAtom } from './atoms'; /** * Returns an action object used to register a new keyboard shortcut. * - * @param {Function} get Atom resover. - * @param {Function} set Atom updater. - * @param {WPShortcutConfig} config Shortcut config. + * @param {WPShortcutConfig} config Shortcut config. + * + * @return {Object} action. */ -export const registerShortcut = ( config ) => ( { get, set } ) => { - const shortcutNames = get( shortcutNamesAtom ); - const hasShortcut = shortcutNames.includes( config.name ); - if ( ! hasShortcut ) { - set( shortcutNamesAtom, [ ...shortcutNames, config.name ] ); - } - const shortcutsByName = get( shortcutsByNameAtom ); - set( shortcutsByNameAtom, { - ...shortcutsByName, - [ config.name ]: config, - } ); -}; +export function registerShortcut( { + name, + category, + description, + keyCombination, + aliases, +} ) { + return { + type: 'REGISTER_SHORTCUT', + name, + category, + keyCombination, + aliases, + description, + }; +} /** * Returns an action object used to unregister a keyboard shortcut. * - * @param {Function} get get atom value. - * @param {Function} set set atom value. - * @param {string} name Shortcut name. + * @param {string} name Shortcut name. + * + * @return {Object} action. */ -export const unregisterShortcut = ( name ) => ( { get, set } ) => { - const shortcutNames = get( shortcutNamesAtom ); - set( - shortcutNamesAtom, - shortcutNames.filter( ( n ) => n !== name ) - ); - const shortcutByNames = get( shortcutsByNameAtom ); - set( shortcutsByNameAtom, omit( shortcutByNames, [ name ] ) ); - - // The atom will remain in the family atoms - // We need to build a way to remove it automatically once the parent atom changes. - // atomRegistry.deleteAtom( shortcutByNames[ name ] ); -}; +export function unregisterShortcut( name ) { + return { + type: 'UNREGISTER_SHORTCUT', + name, + }; +} diff --git a/packages/keyboard-shortcuts/src/store/atoms.js b/packages/keyboard-shortcuts/src/store/atoms.js deleted file mode 100644 index d5a3a159252ef..0000000000000 --- a/packages/keyboard-shortcuts/src/store/atoms.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { createAtom, createDerivedAtom } from '@wordpress/stan'; - -export const shortcutNamesAtom = createAtom( [], { id: 'shortcut-names' } ); -export const shortcutsByNameAtom = createAtom( - {}, - { id: 'shortcuts-by-name' } -); -export const shortcutsAtom = createDerivedAtom( - ( { get } ) => { - const shortcutsByName = get( shortcutsByNameAtom ); - return get( shortcutNamesAtom ).map( ( id ) => shortcutsByName[ id ] ); - }, - () => {}, - { id: 'shortcuts' } -); - -export const rootAtoms = [ shortcutNamesAtom, shortcutsByNameAtom ]; diff --git a/packages/keyboard-shortcuts/src/store/index.js b/packages/keyboard-shortcuts/src/store/index.js index c4f385d89b4b3..0f40e3f4d7482 100644 --- a/packages/keyboard-shortcuts/src/store/index.js +++ b/packages/keyboard-shortcuts/src/store/index.js @@ -1,15 +1,17 @@ /** * WordPress dependencies */ -import { __experimentalCreateAtomicStore, register } from '@wordpress/data'; +import { createReduxStore, register } from '@wordpress/data'; /** * Internal dependencies */ -import { rootAtoms } from './atoms'; +import reducer from './reducer'; import * as actions from './actions'; import * as selectors from './selectors'; +const STORE_NAME = 'core/keyboard-shortcuts'; + /** * Store definition for the keyboard shortcuts namespace. * @@ -17,13 +19,10 @@ import * as selectors from './selectors'; * * @type {Object} */ -export const store = __experimentalCreateAtomicStore( - 'core/keyboard-shortcuts', - { - rootAtoms, - actions, - selectors, - } -); +export const store = createReduxStore( STORE_NAME, { + reducer, + actions, + selectors, +} ); register( store ); diff --git a/packages/keyboard-shortcuts/src/store/reducer.js b/packages/keyboard-shortcuts/src/store/reducer.js new file mode 100644 index 0000000000000..7489d529bf26a --- /dev/null +++ b/packages/keyboard-shortcuts/src/store/reducer.js @@ -0,0 +1,33 @@ +/** + * External dependencies + */ +import { omit } from 'lodash'; + +/** + * Reducer returning the registered shortcuts + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +function reducer( state = {}, action ) { + switch ( action.type ) { + case 'REGISTER_SHORTCUT': + return { + ...state, + [ action.name ]: { + category: action.category, + keyCombination: action.keyCombination, + aliases: action.aliases, + description: action.description, + }, + }; + case 'UNREGISTER_SHORTCUT': + return omit( state, action.name ); + } + + return state; +} + +export default reducer; diff --git a/packages/keyboard-shortcuts/src/store/selectors.js b/packages/keyboard-shortcuts/src/store/selectors.js index 9f467cbeb99e8..4bf4c4c9b9c5a 100644 --- a/packages/keyboard-shortcuts/src/store/selectors.js +++ b/packages/keyboard-shortcuts/src/store/selectors.js @@ -1,6 +1,7 @@ /** * External dependencies */ +import createSelector from 'rememo'; import { compact } from 'lodash'; /** @@ -11,12 +12,6 @@ import { shortcutAriaLabel, rawShortcut, } from '@wordpress/keycodes'; -import { createAtomSelector } from '@wordpress/stan'; - -/** - * Internal dependencies - */ -import { shortcutsAtom, shortcutsByNameAtom } from './atoms'; /** @typedef {import('./actions').WPShortcutKeyCombination} WPShortcutKeyCombination */ @@ -64,105 +59,99 @@ function getKeyCombinationRepresentation( shortcut, representation ) { : shortcut.character; } -/** - * Returns the shortcut object for a given shortcut name. - * - * @param {string} name Shortcut name. - * @return {WPShortcutKeyCombination?} Key combination. - */ -const getShortcut = createAtomSelector( ( name ) => ( { get } ) => { - return get( shortcutsByNameAtom )[ name ]; -} ); - /** * Returns the main key combination for a given shortcut name. * - * @param {string} name Shortcut name. + * @param {Object} state Global state. + * @param {string} name Shortcut name. + * * @return {WPShortcutKeyCombination?} Key combination. */ -export const getShortcutKeyCombination = createAtomSelector( - ( name ) => ( { get } ) => { - const shortcut = get( getShortcut( name ) ); - return shortcut ? shortcut.keyCombination : null; - } -); +export function getShortcutKeyCombination( state, name ) { + return state[ name ] ? state[ name ].keyCombination : null; +} /** * Returns a string representing the main key combination for a given shortcut name. * + * @param {Object} state Global state. * @param {string} name Shortcut name. * @param {keyof FORMATTING_METHODS} representation Type of representation * (display, raw, ariaLabel). * * @return {string?} Shortcut representation. */ -export const getShortcutRepresentation = createAtomSelector( - ( name, representation = 'display' ) => ( { get } ) => { - const shortcut = get( getShortcutKeyCombination( name ) ); - return getKeyCombinationRepresentation( shortcut, representation ); - } -); +export function getShortcutRepresentation( + state, + name, + representation = 'display' +) { + const shortcut = getShortcutKeyCombination( state, name ); + return getKeyCombinationRepresentation( shortcut, representation ); +} /** * Returns the shortcut description given its name. * - * @param {string} name Shortcut name. + * @param {Object} state Global state. + * @param {string} name Shortcut name. * * @return {string?} Shortcut description. */ -export const getShortcutDescription = createAtomSelector( - ( name ) => ( { get } ) => { - const shortcut = get( getShortcut( name ) ); - return shortcut ? shortcut.description : null; - } -); +export function getShortcutDescription( state, name ) { + return state[ name ] ? state[ name ].description : null; +} /** * Returns the aliases for a given shortcut name. * - * @param {string} name Shortcut name. + * @param {Object} state Global state. + * @param {string} name Shortcut name. * * @return {WPShortcutKeyCombination[]} Key combinations. */ -export const getShortcutAliases = createAtomSelector( - ( name ) => ( { get } ) => { - const shortcut = get( getShortcut( name ) ); - return shortcut && shortcut.aliases ? shortcut.aliases : EMPTY_ARRAY; - } -); +export function getShortcutAliases( state, name ) { + return state[ name ] && state[ name ].aliases + ? state[ name ].aliases + : EMPTY_ARRAY; +} /** * Returns the raw representation of all the keyboard combinations of a given shortcut name. * - * @param {string} name Shortcut name. + * @param {Object} state Global state. + * @param {string} name Shortcut name. * * @return {string[]} Shortcuts. */ -export const getAllShortcutRawKeyCombinations = createAtomSelector( - ( name ) => ( { get } ) => { +export const getAllShortcutRawKeyCombinations = createSelector( + ( state, name ) => { return compact( [ getKeyCombinationRepresentation( - get( getShortcutKeyCombination( name ) ), + getShortcutKeyCombination( state, name ), 'raw' ), - ...get( getShortcutAliases( name ) ).map( ( combination ) => + ...getShortcutAliases( state, name ).map( ( combination ) => getKeyCombinationRepresentation( combination, 'raw' ) ), ] ); - } + }, + ( state, name ) => [ state[ name ] ] ); /** * Returns the shortcut names list for a given category name. * - * @param {string} categoryName Category name. + * @param {Object} state Global state. + * @param {string} name Category name. * * @return {string[]} Shortcut names. */ -export const getCategoryShortcuts = createAtomSelector( - ( categoryName ) => ( { get } ) => { - return ( get( shortcutsAtom ) || [] ) - .filter( ( shortcut ) => shortcut.category === categoryName ) - .map( ( { name } ) => name ); - } +export const getCategoryShortcuts = createSelector( + ( state, categoryName ) => { + return Object.entries( state ) + .filter( ( [ , shortcut ] ) => shortcut.category === categoryName ) + .map( ( [ name ] ) => name ); + }, + ( state ) => [ state ] ); diff --git a/packages/redux-routine/src/runtime.js b/packages/redux-routine/src/runtime.js index ac1db84f6c738..d534baae4efc0 100644 --- a/packages/redux-routine/src/runtime.js +++ b/packages/redux-routine/src/runtime.js @@ -46,7 +46,6 @@ export default function createRuntime( controls = {}, dispatch ) { if ( ! isAction( value ) ) { return false; } - dispatch( value ); next(); return true; diff --git a/packages/stan/.npmrc b/packages/stan/.npmrc deleted file mode 100644 index 43c97e719a5a8..0000000000000 --- a/packages/stan/.npmrc +++ /dev/null @@ -1 +0,0 @@ -package-lock=false diff --git a/packages/stan/CHANGELOG.md b/packages/stan/CHANGELOG.md deleted file mode 100644 index 1a910fdfc5f08..0000000000000 --- a/packages/stan/CHANGELOG.md +++ /dev/null @@ -1,5 +0,0 @@ - - -## Unreleased - -Initial release. \ No newline at end of file diff --git a/packages/stan/README.md b/packages/stan/README.md deleted file mode 100644 index 98711d027853e..0000000000000 --- a/packages/stan/README.md +++ /dev/null @@ -1,216 +0,0 @@ -# Stan - -"stan" stands for "state" in Polish 🇵🇱. It's a framework agnostic library to manage distributed state in JavaScript application. It is highly inspired by the equivalent [Recoil](https://recoiljs.org/) and [jotai](https://jotai.surge.sh) - -It share the same goals as Recoil and Jotai: - -- Based on atoms (or observables) which means it's highly performant at scale - only what needs to update gets updated. -- Shares with Jotai the goal of maintaining a very light API surface. -- Supports async and sync state. - -Unlike these frameworks, it has the following goals that justified the creation of a separate library: - -- It is independent from other libraries like React. You can create binding for any of your desired frameworks. -- It needs to be flexible enough to offer bindings for `@wordpress/data` consumer API (`useSelect` and `useDispatch`). - -## Installation - -Install the module - -```bash -npm install @wordpress/stan --save -``` - -_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ - -## Getting started - -Stan is based on the concept of "atoms". Atoms are discrete state units and your application state is a tree of atoms that depend on each other. - -### Creating basic atoms - -Let's create some basic atoms: - -```js -import { createAtom } from '@wordpress/stan'; - -// Creates an atom with an initial value of 1. -// The value can be of any type. -const counter = createAtom( 1 ); -``` - -### Manipulating atoms - -In this example we created an atom that can hold a counter that starts with `1`. -To manipulate we need a registry. The registry is the container of all atom states. - -```js -import { createAtomRegistry } from '@wordpress/stan'; - -const registry = createAtomRegistry(); - -// Read the counter. -console.log( registry.get( counter ) ); // prints 1. - -// Modify the value of the counter -registry.set( counter, 10 ); - -console.log( registry.get( counter ) ); // prints 10. -``` - -### Subscribing to changes - -Each atom is an observable to which we can subscribe: - -```js -registry.subscribe( counter, () => { - console.log( registry.get( counter ) ); -} ); - -registry.set( counter, 2 ); // prints 2. -registry.set( counter, 4 ); // prints 4. -``` - -### Derived atoms - -Atoms can also derive their value based on other atoms. We call these "derived atoms". - -```js -import { createAtom, createDerivedAtom } from '@wordpress/stan'; - -const counter1 = createAtom( 1 ); -const counter2 = createAtom( 2 ); -const sum = createDerivedAtom( - ( { get } ) => get( counter1 ) + get( counter2 ) -); -``` - -In the example above, we create two simple counter atoms and third derived "sum" atom which value is the sum of both counters. - -```js -console.log( registry.get( sum ) ); // prints 3. - -// Adding a listener automatically triggers the refreshing of the value. -// If the atom has no subscriber, it will only attempt a resolution when initially read. -// But it won't bother refreshing its value, if any of its dependencies change. -// This property (laziness) is important for performance reasons. -registry.subscribe( sum, () => { - console.log( registry.get( sum ) ); -} ); - -// This edits counter1, triggering a resolution of sumInstance which triggers the console.log above. -registry.set( counter1, 2 ); // now both counters equal 2 which means sum will print 4. -registry.set( counter1, 4 ); // prints 6 -``` - -### Async derived atoms - -Derived atoms can use async functions to compute their values. They can for instance trigger some REST API call and returns a promise. - -```js -const sum2 = createDerivedAtom( - async ( { get } ) => { - const val1 = await Promise.resolve( 10 ); - return val1 * get( counter ); - } -); -``` - -The value of async atoms will be equal to `null` until the resolution function finishes. - -### Bindings - -It is important to note that stan instance and registries are low-level APIs meant to be used by developers to build bindings for frameworks of their choice. In general, a higher-level API is preferred. - -Currently available bindings: - -- `@wordpress/data`: WordPress data users can continue to use their existing high-level APIs `useSelect`/`useDispatch` (selectors and actions) to access the atoms. The selectors are just high-level atoms that can rely on lower-level ones and the actions are just functions that trigger atom setters. The API for `@wordpress/data` store authors to bridge the gap is still experimental. - -## API Reference - - - -# **createAtom** - -Creates a basic atom. - -_Parameters_ - -- _initialValue_ `T`: Initial value in the atom. \* -- _config_ `[WPCommonAtomConfig]`: Common Atom config. - -_Returns_ - -- `WPAtom`: Created atom. - -# **createAtomRegistry** - -Creates a new Atom Registry. - -_Parameters_ - -- _onAdd_ `RegistryListener`: -- _onDelete_ `RegistryListener`: - -_Returns_ - -- `WPAtomRegistry`: Atom Registry. - -# **createAtomSelector** - -_Parameters_ - -- _resolver_ `WPAtomSelectorResolver`: Atom resolver. -- _updater_ `WPAtomSelectorUpdater`: Atom updater. -- _atomConfig_ `[WPCommonAtomConfig]`: Common Atom config. - -_Returns_ - -- (unknown type): Atom selector creator. - -# **createDerivedAtom** - -Creates a derived atom. - -_Parameters_ - -- _resolver_ `WPDerivedAtomResolver`: Atom resolver. -- _updater_ `WPDerivedAtomUpdater`: Atom updater. -- _config_ `[WPCommonAtomConfig]`: Common Atom config. - -_Returns_ - -- `WPAtom`: Created atom. - -# **createStoreAtom** - -Creates a store atom. - -_Parameters_ - -- _subscribe_ (unknown type): Subscribe to state changes. -- _get_ (unknown type): Get the state value. -- _dispatch_ (unknown type): Dispatch store changes, -- _config_ `[WPCommonAtomConfig]`: Common Atom config. - -_Returns_ - -- `WPAtom`: Store Atom. - -# **createStoreAtomSelector** - -_Parameters_ - -- _subscribe_ (unknown type): Subscribe to state changes. -- _get_ (unknown type): Get the state value. -- _dispatch_ (unknown type): Dispatch store changes, -- _atomConfig_ `[WPCommonAtomConfig]`: Common Atom config. - -_Returns_ - -- (unknown type): Atom selector creator. - - - - -

Code is Poetry.

diff --git a/packages/stan/package.json b/packages/stan/package.json deleted file mode 100644 index f5150de878443..0000000000000 --- a/packages/stan/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@wordpress/stan", - "version": "0.0.1", - "description": "WordPress state library based on atoms.", - "author": "The WordPress Contributors", - "license": "GPL-2.0-or-later", - "keywords": [ - "wordpress", - "gutenberg", - "state", - "stan", - "recoil" - ], - "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/stan/README.md", - "repository": { - "type": "git", - "url": "https://github.com/WordPress/gutenberg.git", - "directory": "packages/stan" - }, - "bugs": { - "url": "https://github.com/WordPress/gutenberg/issues" - }, - "main": "build/index.js", - "module": "build-module/index.js", - "react-native": "src/index", - "types": "build-types", - "sideEffects": false, - "dependencies": { - "@babel/runtime": "^7.11.2", - "@wordpress/priority-queue": "file:../priority-queue", - "equivalent-key-map": "^0.2.2", - "lodash": "^4.17.19" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/stan/src/atom.js b/packages/stan/src/atom.js deleted file mode 100644 index a4ecda1c87400..0000000000000 --- a/packages/stan/src/atom.js +++ /dev/null @@ -1,44 +0,0 @@ -/** @typedef {import('./types').WPCommonAtomConfig} WPCommonAtomConfig */ -/** - * @template T - * @typedef {import("./types").WPAtom} WPAtom - */ - -/** - * Creates a basic atom. - * - * @template T - * @param {T} initialValue Initial value in the atom. * - * @param {WPCommonAtomConfig=} config Common Atom config. - * @return {WPAtom} Created atom. - */ -export const createAtom = ( initialValue, config = {} ) => () => { - let value = initialValue; - - /** - * @type {Set<() => void>} - */ - const listeners = new Set(); - - return { - id: config.id, - type: 'root', - set( newValue ) { - value = newValue; - listeners.forEach( ( l ) => l() ); - }, - get() { - return value; - }, - async resolve() { - return value; - }, - subscribe( listener ) { - listeners.add( listener ); - return () => { - listeners.delete( listener ); - }; - }, - isResolved: true, - }; -}; diff --git a/packages/stan/src/derived.js b/packages/stan/src/derived.js deleted file mode 100644 index c678cf187f555..0000000000000 --- a/packages/stan/src/derived.js +++ /dev/null @@ -1,203 +0,0 @@ -/** - * External dependencies - */ -import { noop } from 'lodash'; - -/** - * WordPress dependencies - */ -import { createQueue } from '@wordpress/priority-queue'; - -const resolveQueue = createQueue(); - -/** @typedef {import('./types').WPCommonAtomConfig} WPCommonAtomConfig */ -/** - * @template T - * @typedef {import("./types").WPDerivedAtomResolver} WPDerivedAtomResolver - */ -/** - * @template T - * @typedef {import("./types").WPDerivedAtomUpdater} WPDerivedAtomUpdater - */ -/** - * @template T - * @typedef {import("./types").WPAtom} WPAtom - */ -/** - * @template T - * @typedef {import("./types").WPAtomState} WPAtomState - */ - -/** - * Creates a derived atom. - * - * @template T - * @param {WPDerivedAtomResolver} resolver Atom resolver. - * @param {WPDerivedAtomUpdater} updater Atom updater. - * @param {WPCommonAtomConfig=} config Common Atom config. - * @return {WPAtom} Created atom. - */ -export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( - registry -) => { - /** - * @type {any} - */ - let value = null; - - /** - * @type {Set<() => void>} - */ - const listeners = new Set(); - - /** - * @type {(WPAtomState)[]} - */ - let dependencies = []; - let isListening = false; - let isResolved = false; - const context = {}; - - const dependenciesUnsubscribeMap = new WeakMap(); - - const notifyListeners = () => { - listeners.forEach( ( l ) => l() ); - }; - - const refresh = () => { - if ( listeners.size > 0 ) { - if ( config.isAsync ) { - resolveQueue.add( context, resolve ); - } else { - resolve(); - } - } else { - isListening = false; - } - }; - - /** - * @param {WPAtomState} atomState - */ - const addDependency = ( atomState ) => { - if ( ! dependenciesUnsubscribeMap.has( atomState ) ) { - dependenciesUnsubscribeMap.set( - atomState, - atomState.subscribe( refresh ) - ); - } - }; - - const resolve = () => { - /** - * @type {(WPAtomState)[]} - */ - const updatedDependencies = []; - const updatedDependenciesMap = new WeakMap(); - let result; - const unresolved = []; - try { - result = resolver( { - get: ( atom ) => { - const atomState = registry.__unstableGetAtomState( atom ); - // It is important to add the dependency as soon as it's used - // because it's important to retrigger the resolution if the dependency - // changes before the resolution finishes. - addDependency( atomState ); - updatedDependenciesMap.set( atomState, true ); - updatedDependencies.push( atomState ); - const ret = atomState.get(); - if ( ! atomState.isResolved ) { - unresolved.push( atomState ); - } - return ret; - }, - } ); - } catch ( error ) { - if ( unresolved.length === 0 ) { - throw error; - } - } - - function removeExtraDependencies() { - const removedDependencies = dependencies.filter( - ( d ) => ! updatedDependenciesMap.has( d ) - ); - dependencies = updatedDependencies; - removedDependencies.forEach( ( d ) => { - const unsubscribe = dependenciesUnsubscribeMap.get( d ); - dependenciesUnsubscribeMap.delete( d ); - if ( unsubscribe ) { - unsubscribe(); - } - } ); - } - - /** - * @param {any} newValue - */ - function checkNewValue( newValue ) { - if ( unresolved.length === 0 ) { - isResolved = true; - } - - if ( unresolved.length === 0 && newValue !== value ) { - value = newValue; - notifyListeners(); - } - } - - if ( result instanceof Promise ) { - // Should make this promise cancelable. - result - .then( ( newValue ) => { - removeExtraDependencies(); - checkNewValue( newValue ); - } ) - .catch( ( error ) => { - if ( unresolved.length === 0 ) { - throw error; - } - removeExtraDependencies(); - } ); - } else { - removeExtraDependencies(); - checkNewValue( result ); - } - }; - - return { - id: config.id, - type: 'derived', - get() { - if ( ! isListening ) { - isListening = true; - resolve(); - } - return value; - }, - async set( action ) { - await updater( - { - get: ( atom ) => registry.get( atom ), - set: ( atom, arg ) => registry.set( atom, arg ), - }, - action - ); - }, - resolve, - subscribe( listener ) { - if ( ! isListening ) { - isListening = true; - resolve(); - } - listeners.add( listener ); - return () => { - listeners.delete( listener ); - }; - }, - get isResolved() { - return isResolved; - }, - }; -}; diff --git a/packages/stan/src/index.js b/packages/stan/src/index.js deleted file mode 100644 index e7f82fb62126e..0000000000000 --- a/packages/stan/src/index.js +++ /dev/null @@ -1,25 +0,0 @@ -export { createAtom } from './atom'; -export { createDerivedAtom } from './derived'; -export { createAtomSelector } from './selector'; -export { createStoreAtom, createStoreAtomSelector } from './store'; -export { createAtomRegistry } from './registry'; - -/** - * @template T - * @typedef {import('./types').WPAtom} WPAtom - */ -/** - * @template T - * @typedef {import('./types').WPAtomSelector} WPAtomSelector - */ -/** - * @template T - * @typedef {import('./types').WPAtomResolver} WPAtomResolver - */ -/** - * @typedef {import('./types').WPAtomRegistry} WPAtomRegistry - */ -/** - * @template T - * @typedef {import('./types').WPAtomUpdater} WPAtomUpdater - */ diff --git a/packages/stan/src/registry.js b/packages/stan/src/registry.js deleted file mode 100644 index 3961e3c4f24fd..0000000000000 --- a/packages/stan/src/registry.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * External dependencies - */ -import { noop, isObject } from 'lodash'; -// @ts-ignore -import EquivalentKeyMap from 'equivalent-key-map'; - -/** @typedef {import('./types').WPAtomRegistry} WPAtomRegistry */ -/** - * @template T - * @typedef {import("./types").WPAtomSelectorResolver} WPAtomSelectorResolver - */ -/** - * @template T - * @typedef {import("./types").WPAtomState} WPAtomState - */ -/** - * @template T - * @typedef {import("./types").WPAtom} WPAtom - */ -/** - * @template T - * @typedef {import("./types").WPAtomSelector} WPAtomSelector - */ -/** - * @template T - * @typedef {import("./types").WPAtomSelectorConfig} WPAtomSelectorConfig - */ -/** - * @typedef {( atomState: import('./types').WPAtomState ) => void} RegistryListener - */ - -/** - * @template T - * @param {WPAtom|WPAtomSelector} maybeAtomSelector - * @return {boolean} maybeAtomSelector Returns `true` when atom selector detected. - */ -export function isAtomSelector( maybeAtomSelector ) { - return ( - isObject( maybeAtomSelector ) && - /** @type {WPAtomSelector} */ ( maybeAtomSelector ).type === - 'selector' - ); -} - -/** - * Creates a new Atom Registry. - * - * @param {RegistryListener} onAdd - * @param {RegistryListener} onDelete - * - * @return {WPAtomRegistry} Atom Registry. - */ -export const createAtomRegistry = ( onAdd = noop, onDelete = noop ) => { - const atoms = new WeakMap(); - const selectors = new WeakMap(); - - /** - * @template T - * @param {WPAtom|WPAtomSelector} atom Atom. - * @return {WPAtomState} Atom state. - */ - const getAtomState = ( atom ) => { - if ( isAtomSelector( atom ) ) { - const { - config, - args, - } = /** @type {WPAtomSelector} */ ( atom ); - return selectorRegistry.getAtomSelector( config, args ); - } - - if ( ! atoms.get( atom ) ) { - const atomState = /** @type {WPAtom} */ ( atom )( registry ); - atoms.set( atom, atomState ); - onAdd( atomState ); - } - - return atoms.get( atom ); - }; - - const selectorRegistry = { - /** - * @template T - * @param {WPAtomSelectorConfig} atomSelectorConfig - * @param {any[]} args - * @return {WPAtomState} Atom state. - */ - getAtomSelector( atomSelectorConfig, args ) { - if ( ! selectors.get( atomSelectorConfig ) ) { - selectors.set( atomSelectorConfig, new EquivalentKeyMap() ); - } - - const selectorsMap = selectors.get( atomSelectorConfig ); - if ( ! selectorsMap.has( args ) ) { - const atomCreator = atomSelectorConfig.createAtom( ...args ); - selectorsMap.set( args, atomCreator( registry ) ); - } - - return selectorsMap.get( args ); - }, - - /** - * @template T - * @param {WPAtomSelectorConfig} atomSelectorConfig - * @param {any[]} args - */ - deleteAtomSelector( atomSelectorConfig, args ) { - if ( - selectors.has( atomSelectorConfig ) && - selectors.get( atomSelectorConfig ).has( args ) - ) { - selectors.get( atomSelectorConfig ).delete( args ); - } - }, - }; - - /** @type {WPAtomRegistry} */ - const registry = { - __unstableGetAtomState: getAtomState, - - get( atom ) { - return getAtomState( atom ).get(); - }, - - set( atom, value ) { - return getAtomState( atom ).set( value ); - }, - - subscribe( atom, listener ) { - return getAtomState( atom ).subscribe( listener ); - }, - - // This shouldn't be necessary since we rely on week map - // But the legacy selectors/actions API requires us to know when - // some atoms are removed entirely to unsubscribe. - delete( atom ) { - if ( isAtomSelector( atom ) ) { - const { - config, - args, - } = /** @type {WPAtomSelector} */ ( atom ); - return selectorRegistry.deleteAtomSelector( config, args ); - } - const atomState = atoms.get( atom ); - atoms.delete( atom ); - onDelete( atomState ); - }, - }; - - return registry; -}; diff --git a/packages/stan/src/selector.js b/packages/stan/src/selector.js deleted file mode 100644 index d7955db33e8bc..0000000000000 --- a/packages/stan/src/selector.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Internal dependencies - */ -import { createDerivedAtom } from './derived'; - -/** @typedef {import('./types').WPCommonAtomConfig} WPCommonAtomConfig */ -/** - * @template T - * @typedef {import("./types").WPAtomSelectorResolver} WPAtomSelectorResolver - */ -/** - * @template T - * @typedef {import("./types").WPAtomSelectorUpdater} WPAtomSelectorUpdater - */ -/** - * @template T - * @typedef {import("./types").WPAtom} WPAtom - */ -/** - * @template T - * @typedef {import("./types").WPAtomSelector} WPAtomSelector - */ - -/** - * @template T - * @param {WPAtomSelectorResolver} resolver Atom resolver. - * @param {WPAtomSelectorUpdater} updater Atom updater. - * @param {WPCommonAtomConfig=} atomConfig Common Atom config. - * @return {(...args:any[]) => WPAtomSelector} Atom selector creator. - */ -export const createAtomSelector = ( resolver, updater, atomConfig = {} ) => { - const config = { - /** - * @param {...any[]} args Selector arguments - * @return {WPAtom} Atom. - */ - createAtom( ...args ) { - return createDerivedAtom( - resolver( ...args ), - updater ? updater( ...args ) : undefined, - { - ...atomConfig, - } - ); - }, - }; - - return ( ...args ) => { - return { - type: 'selector', - config, - args, - }; - }; -}; diff --git a/packages/stan/src/store.js b/packages/stan/src/store.js deleted file mode 100644 index 6fd74103122c2..0000000000000 --- a/packages/stan/src/store.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @template T - * @typedef {import("./types").WPAtom} WPAtom - */ -/** - * @typedef {import("./types").WPCommonAtomConfig} WPCommonAtomConfig - */ -/** - * @template T - * @typedef {import("./types").WPAtomSelector} WPAtomSelector - */ - -/** - * Creates a store atom. - * - * @template T - * @param {(listener: () => void) => (() => void)} subscribe Subscribe to state changes. - * @param {() => T} get Get the state value. - * @param {(action: any) => void} dispatch Dispatch store changes, - * @param {WPCommonAtomConfig=} config Common Atom config. - * @return {WPAtom} Store Atom. - */ -export const createStoreAtom = ( - subscribe, - get, - dispatch, - config = {} -) => () => { - return { - id: config.id, - type: 'store', - get() { - return get(); - }, - set( action ) { - dispatch( action ); - }, - subscribe: ( l ) => { - return subscribe( l ); - }, - isResolved: true, - }; -}; - -/** - * @template T - * @param {(...args:any[]) => (listener: () => void) => (() => void)} subscribe Subscribe to state changes. - * @param {(...args:any[]) => () => T} get Get the state value. - * @param {(...args:any[]) => (action: any) => void} dispatch Dispatch store changes, - * @param {WPCommonAtomConfig=} atomConfig Common Atom config. - * @return {(...args:any[]) => WPAtomSelector} Atom selector creator. - */ -export const createStoreAtomSelector = ( - subscribe, - get, - dispatch, - atomConfig -) => { - const config = { - /** - * @param {...any[]} args Selector arguments - * @return {WPAtom} Atom. - */ - createAtom( ...args ) { - return createStoreAtom( - subscribe( ...args ), - get( ...args ), - dispatch( ...args ), - { - ...atomConfig, - } - ); - }, - }; - - return ( ...args ) => { - return { - type: 'selector', - config, - args, - }; - }; -}; diff --git a/packages/stan/src/test/atom.js b/packages/stan/src/test/atom.js deleted file mode 100644 index 74985576995fe..0000000000000 --- a/packages/stan/src/test/atom.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Internal dependencies - */ -import { createAtomRegistry, createAtom } from '../'; - -describe( 'atoms', () => { - it( 'should allow getting and setting atom values', () => { - const count = createAtom( 1 ); - const registry = createAtomRegistry(); - expect( registry.get( count ) ).toEqual( 1 ); - registry.set( count, 2 ); - expect( registry.get( count ) ).toEqual( 2 ); - } ); - - it( 'should allow subscribing to atom changes', () => { - const count = createAtom( 1 ); - const registry = createAtomRegistry(); - const listener = jest.fn(); - registry.subscribe( count, listener ); - expect( registry.get( count ) ).toEqual( 1 ); - registry.set( count, 2 ); - expect( listener ).toHaveBeenCalledTimes( 1 ); - expect( registry.get( count ) ).toEqual( 2 ); - registry.set( count, 3 ); - expect( listener ).toHaveBeenCalledTimes( 2 ); - expect( registry.get( count ) ).toEqual( 3 ); - } ); -} ); diff --git a/packages/stan/src/test/derived.js b/packages/stan/src/test/derived.js deleted file mode 100644 index fc2c2777c970d..0000000000000 --- a/packages/stan/src/test/derived.js +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Internal dependencies - */ -import { createAtomRegistry, createAtom, createDerivedAtom } from '../'; - -async function flushImmediatesAndTicks( count = 1 ) { - for ( let i = 0; i < count; i++ ) { - await jest.runAllTicks(); - await jest.runAllImmediates(); - } -} - -jest.useFakeTimers(); - -describe( 'creating derived atoms', () => { - it( 'should allow creating derived atom', async () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 1 ); - const count3 = createAtom( 1 ); - const sum = createDerivedAtom( - ( { get } ) => get( count1 ) + get( count2 ) + get( count3 ) - ); - const registry = createAtomRegistry(); - - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( sum, () => {} ); - expect( registry.get( sum ) ).toEqual( 3 ); - registry.set( count1, 2 ); - expect( registry.get( sum ) ).toEqual( 4 ); - unsubscribe(); - } ); - - it( 'should allow async derived atoms', async () => { - const count1 = createAtom( 1 ); - const sum = createDerivedAtom( async ( { get } ) => { - const value = await Promise.resolve( 10 ); - return get( count1 ) + value; - } ); - const registry = createAtomRegistry(); - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( sum, () => {} ); - await flushImmediatesAndTicks(); - expect( registry.get( sum ) ).toEqual( 11 ); - unsubscribe(); - } ); - - it( 'should allow parallel async atoms', async () => { - const count1 = createDerivedAtom( async () => { - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - return 10; - } ); - const count2 = createDerivedAtom( async () => { - await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); - return 10; - } ); - const sum = createDerivedAtom( async ( { get } ) => { - return get( count1 ) + get( count2 ); - } ); - const registry = createAtomRegistry(); - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( sum, () => {} ); - await jest.advanceTimersByTime( 1000 ); - await flushImmediatesAndTicks(); - expect( registry.get( sum ) ).toEqual( 20 ); - unsubscribe(); - } ); - - it( 'should allow nesting derived atoms', async () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 10 ); - const asyncCount = createDerivedAtom( async ( { get } ) => { - return ( await get( count2 ) ) * 2; - } ); - const sum = createDerivedAtom( async ( { get } ) => { - return get( count1 ) + get( asyncCount ); - } ); - const registry = createAtomRegistry(); - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( sum, () => {} ); - await flushImmediatesAndTicks( 2 ); - expect( registry.get( sum ) ).toEqual( 21 ); - unsubscribe(); - } ); - - it( 'should only compute derived atoms when they have subscribers or when you try to retrieve their value', () => { - const mock = jest.fn(); - mock.mockImplementation( () => 10 ); - const count1 = createAtom( 1 ); - const count2 = createAtom( 1 ); - const sum = createDerivedAtom( - ( { get } ) => get( count1 ) + get( count2 ) + mock() - ); - const registry = createAtomRegistry(); - // Creating an atom or adding it to the registry don't trigger its resolution - expect( mock ).not.toHaveBeenCalled(); - expect( registry.get( sum ) ).toEqual( 12 ); - // Calling "get" triggers a resolution. - expect( mock ).toHaveBeenCalledTimes( 1 ); - - // This shouldn't trigger the resolution because the atom has no listener. - registry.set( count1, 2 ); - expect( mock ).toHaveBeenCalledTimes( 1 ); - - // Subscribing triggers the resolution again. - const unsubscribe = registry.subscribe( sum, () => {} ); - expect( mock ).toHaveBeenCalledTimes( 2 ); - expect( registry.get( sum ) ).toEqual( 13 ); - unsubscribe(); - } ); - - it( 'should notify subscribers on change', () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 1 ); - const sum = createDerivedAtom( - ( { get } ) => get( count1 ) + get( count2 ) - ); - const registry = createAtomRegistry(); - const listener = jest.fn(); - const unsubscribe = registry.subscribe( sum, listener ); - - registry.set( count1, 2 ); - expect( listener ).toHaveBeenCalledTimes( 1 ); - - registry.set( count2, 2 ); - expect( listener ).toHaveBeenCalledTimes( 2 ); - - unsubscribe(); - } ); -} ); - -describe( 'updating derived atoms', () => { - it( 'should allow derived atoms to update dependencies', () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 1 ); - const sum = createDerivedAtom( - ( { get } ) => get( count1 ) + get( count2 ), - ( { set }, value ) => { - set( count1, value / 2 ); - set( count2, value / 2 ); - } - ); - const registry = createAtomRegistry(); - registry.set( sum, 4 ); - expect( registry.get( count1 ) ).toEqual( 2 ); - expect( registry.get( count2 ) ).toEqual( 2 ); - } ); - - it( 'should allow nested derived atoms to update dependencies', () => { - const count1 = createAtom( 1 ); - const count2 = createAtom( 1 ); - const sum = createDerivedAtom( - ( { get } ) => get( count1 ) + get( count2 ), - ( { set }, value ) => { - set( count1, value / 2 ); - set( count2, value / 2 ); - } - ); - const multiply = createDerivedAtom( - ( { get } ) => get( sum ) * 3, - ( { set }, value ) => { - set( sum, value / 3 ); - } - ); - const registry = createAtomRegistry(); - registry.set( multiply, 18 ); - expect( registry.get( count1 ) ).toEqual( 3 ); - expect( registry.get( count2 ) ).toEqual( 3 ); - } ); -} ); diff --git a/packages/stan/src/test/selector.js b/packages/stan/src/test/selector.js deleted file mode 100644 index 3483e02f56f05..0000000000000 --- a/packages/stan/src/test/selector.js +++ /dev/null @@ -1,195 +0,0 @@ -/** - * Internal dependencies - */ -import { createAtomRegistry, createAtom, createAtomSelector } from '../'; - -describe( 'creating and subscribing to atom selectoors', () => { - it( 'should allow adding and removing items into/from selectors', () => { - const itemsByIdAtom = createAtom( {} ); - const itemSelector = createAtomSelector( ( key ) => ( { get } ) => - get( itemsByIdAtom )[ key ] - ); - - const registry = createAtomRegistry(); - // Retrieve atom selector - const firstItem = registry.__unstableGetAtomState( itemSelector( 1 ) ); - expect( firstItem ).toBe( - registry.__unstableGetAtomState( itemSelector( 1 ) ) - ); - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( itemSelector( 1 ), () => {} ); - expect( firstItem.get() ).toBe( undefined ); - - // Add some items - registry.set( itemsByIdAtom, { - 1: { name: 'first' }, - 2: { name: 'second' }, - } ); - - // Should update the value automatically as we set the items. - expect( registry.get( itemSelector( 1 ) ) ).toEqual( { - name: 'first', - } ); - - // Remove items - registry.set( itemsByIdAtom, { - 2: { name: 'second' }, - } ); - - // Should update the value automatically as we unset the items. - expect( registry.get( itemSelector( 1 ) ) ).toBe( undefined ); - unsubscribe(); - } ); - - it( 'should allow creating selectors based on other selectors', () => { - const itemsByIdAtom = createAtom( {} ); - const itemSelector = createAtomSelector( ( key ) => ( { get } ) => { - return get( itemsByIdAtom )[ key ]; - } ); - // Atom selector that depends on another atom selector. - const itemNameSelector = createAtomSelector( ( key ) => ( { get } ) => { - return get( itemSelector( key ) )?.name; - } ); - - const registry = createAtomRegistry(); - registry.set( itemsByIdAtom, { - 1: { name: 'first' }, - 2: { name: 'second' }, - } ); - - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( - itemNameSelector( 1 ), - () => {} - ); - expect( registry.get( itemNameSelector( 1 ) ) ).toEqual( 'first' ); - unsubscribe(); - } ); - - it( 'should allow creating multi argument atom selectors', () => { - const itemsByIdAtom = createAtom( {} ); - const itemPropertySelector = createAtomSelector( - ( key, property ) => ( { get } ) => { - return get( itemsByIdAtom )[ key ][ property ]; - } - ); - - const registry = createAtomRegistry(); - registry.set( itemsByIdAtom, { - 1: { name: 'first' }, - 2: { name: 'second' }, - } ); - - // Retrieve atom selector - const firstItemName = registry.__unstableGetAtomState( - itemPropertySelector( 1, 'name' ) - ); - expect( firstItemName ).toBe( - registry.__unstableGetAtomState( itemPropertySelector( 1, 'name' ) ) - ); - - // Atoms don't compute any value unless there's a subscriber. - const unsubscribe = registry.subscribe( - itemPropertySelector( 1, 'name' ), - () => {} - ); - expect( registry.get( itemPropertySelector( 1, 'name' ) ) ).toEqual( - 'first' - ); - registry.set( itemsByIdAtom, { - 1: { name: 'first modified' }, - 2: { name: 'second' }, - } ); - expect( registry.get( itemPropertySelector( 1, 'name' ) ) ).toEqual( - 'first modified' - ); - unsubscribe(); - } ); - - it( 'should not recompute a selector dependency if its untouched', () => { - const itemsByIdAtom = createAtom( {} ); - const itemSelector = createAtomSelector( ( key ) => ( { get } ) => { - return get( itemsByIdAtom )[ key ]; - } ); - const itemNameSelector = createAtomSelector( ( key ) => ( { get } ) => { - return get( itemSelector( key ) )?.name; - } ); - - const registry = createAtomRegistry(); - const initialItems = { - 1: { name: 'first' }, - 2: { name: 'second' }, - }; - registry.set( itemsByIdAtom, initialItems ); - - const name1Listener = jest.fn(); - const name2Listener = jest.fn(); - - const name1 = itemNameSelector( 1 ); - const name2 = itemNameSelector( 2 ); - - const unsubscribe = registry.subscribe( name1, name1Listener ); - const unsubscribe2 = registry.subscribe( name2, name2Listener ); - - // If I update item 1, item 2 dedendencies shouldn't recompute. - registry.set( itemsByIdAtom, { - ...initialItems, - 1: { name: 'updated first' }, - } ); - - expect( registry.get( name1 ) ).toEqual( 'updated first' ); - expect( registry.get( name2 ) ).toEqual( 'second' ); - expect( name1Listener ).toHaveBeenCalledTimes( 1 ); - expect( name2Listener ).not.toHaveBeenCalled(); - - unsubscribe(); - unsubscribe2(); - } ); -} ); - -describe( 'updating atom selectors', () => { - it( 'should allow atom selectors to update dependencies', () => { - const itemsByIdAtom = createAtom( {} ); - const itemSelector = createAtomSelector( - ( key ) => ( { get } ) => get( itemsByIdAtom )[ key ], - ( key ) => ( { get, set }, value ) => { - set( itemsByIdAtom, { - ...get( itemsByIdAtom ), - [ key ]: value, - } ); - } - ); - const registry = createAtomRegistry(); - registry.set( itemSelector( 1 ), { name: 'first' } ); - expect( registry.get( itemsByIdAtom ) ).toEqual( { - 1: { name: 'first' }, - } ); - } ); - - it( 'should allow updating nested atom selectors', () => { - const itemsByIdAtom = createAtom( {} ); - const itemSelector = createAtomSelector( - ( key ) => ( { get } ) => get( itemsByIdAtom )[ key ], - ( key ) => ( { get, set }, value ) => { - set( itemsByIdAtom, { - ...get( itemsByIdAtom ), - [ key ]: value, - } ); - } - ); - const itemNameSelector = createAtomSelector( - ( key ) => ( { get } ) => get( itemSelector( key ) ).name, - ( key ) => ( { get, set }, value ) => { - set( itemSelector( key ), { - ...get( itemSelector( key ) ), - name: value, - } ); - } - ); - const registry = createAtomRegistry(); - registry.set( itemNameSelector( 1 ), 'first' ); - expect( registry.get( itemsByIdAtom ) ).toEqual( { - 1: { name: 'first' }, - } ); - } ); -} ); diff --git a/packages/stan/src/types.ts b/packages/stan/src/types.ts deleted file mode 100644 index 6656699590022..0000000000000 --- a/packages/stan/src/types.ts +++ /dev/null @@ -1,130 +0,0 @@ -export type WPAtomListener = () => void; - -export interface WPCommonAtomConfig { - /** - * Optinal id used for debug. - */ - id?: string; - - /** - * Whether the atom is sync or async. - */ - isAsync?: boolean; -} - -export interface WPAtomState< T > { - /** - * Optional atom id used for debug. - */ - id?: string; - - /** - * Atom type. - */ - type: string; - - /** - * Whether the atom state value is resolved or not. - */ - readonly isResolved: boolean; - - /** - * Atom state setter, used to modify one or multiple atom values. - */ - set: ( t: any ) => void; - - /** - * Retrieves the current value of the atom state. - */ - get: () => T; - - /** - * Subscribes to the value changes of the atom state. - */ - subscribe: ( listener: WPAtomListener ) => () => void; -} - -export type WPAtom< T > = ( registry: WPAtomRegistry ) => WPAtomState< T >; - -export interface WPAtomSelectorConfig< T > { - /** - * Creates an atom for the given key - */ - createAtom: ( ...args: any[] ) => WPAtom< T >; -} - -export interface WPAtomSelector< T > { - /** - * Type which value is "selector" to indicate that this is a selector. - */ - type: string; - - /** - * Selector config used for this item. - */ - config: WPAtomSelectorConfig< T >; - - /** - * Selector args - */ - args: any[]; -} - -export interface WPAtomRegistry { - /** - * Reads an atom vale. - */ - get: < T >( atom: WPAtom< T > | WPAtomSelector< T > ) => T; - - /** - * Update an atom value. - */ - set: < T >( atom: WPAtom< T > | WPAtomSelector< T >, value: any ) => void; - - /** - * Retrieves or creates an atom from the registry. - */ - subscribe: < T >( - atom: WPAtom< T > | WPAtomSelector< T >, - listener: WPAtomListener - ) => () => void; - - /** - * Removes an atom from the registry. - */ - delete: < T >( atom: WPAtom< T > | WPAtomSelector< T > ) => void; - - /** - * Retrieves the atom state for a given atom. - * This shouldn't be used directly, prefer the other methods. - */ - __unstableGetAtomState: < T >( - atom: WPAtom< T > | WPAtomSelector< T > - ) => WPAtomState< any >; -} - -export type WPAtomResolver< T > = ( - atom: WPAtom< T > | WPAtomSelector< T > -) => T; - -export type WPAtomUpdater< T > = ( - atom: WPAtom< T > | WPAtomSelector< T >, - value: any -) => void; - -export type WPDerivedAtomResolver< T > = ( props: { - get: WPAtomResolver< T >; -} ) => T; - -export type WPDerivedAtomUpdater< T > = ( - props: { get: WPAtomResolver< T >; set: WPAtomUpdater< T > }, - value: any -) => void; - -export type WPAtomSelectorResolver< T > = ( - ...args: any[] -) => WPDerivedAtomResolver< T >; - -export type WPAtomSelectorUpdater< T > = ( - ...args: any[] -) => WPDerivedAtomUpdater< T >; diff --git a/packages/stan/tsconfig.json b/packages/stan/tsconfig.json deleted file mode 100644 index 397b9f48383d5..0000000000000 --- a/packages/stan/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "declarationDir": "build-types" - }, - "include": [ "src/**/*" ], - "references": [ - { "path": "../priority-queue" } - ] -} diff --git a/tsconfig.base.json b/tsconfig.base.json index 94d0f395dc985..fbaf60c3fdd52 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -13,9 +13,6 @@ "emitDeclarationOnly": true, "isolatedModules": true, - /* Prevent type-only imports from impacting the dependency graph */ - "importsNotUsedAsValues": "error", - /* Strict Type-Checking Options */ "strict": true, diff --git a/tsconfig.json b/tsconfig.json index 22e0a1276f521..81433d8d0cd32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ { "path": "packages/blob" }, { "path": "packages/block-editor" }, { "path": "packages/components" }, - { "path": "packages/data" }, { "path": "packages/deprecated" }, { "path": "packages/element" }, { "path": "packages/dependency-extraction-webpack-plugin" }, @@ -23,7 +22,6 @@ { "path": "packages/primitives" }, { "path": "packages/priority-queue" }, { "path": "packages/project-management-automation" }, - { "path": "packages/stan" }, { "path": "packages/token-list" }, { "path": "packages/url" }, { "path": "packages/warning" }, diff --git a/webpack.config.js b/webpack.config.js index 511667e05ac0b..9bb0bcde35b2f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -30,11 +30,7 @@ const { } = process.env; const WORDPRESS_NAMESPACE = '@wordpress/'; -const BUNDLED_PACKAGES = [ - '@wordpress/icons', - '@wordpress/interface', - '@wordpress/stan', -]; +const BUNDLED_PACKAGES = [ '@wordpress/icons', '@wordpress/interface' ]; const gutenbergPackages = Object.keys( dependencies ) .filter( From ad45c4865b247ed4c39ede0c6ed9275e64d4863d Mon Sep 17 00:00:00 2001 From: Luke Walczak Date: Mon, 23 Nov 2020 17:05:07 +0100 Subject: [PATCH 010/317] [RNMobile] Make height conversion test ios only (#27195) --- .../gutenberg-editor-cover.test.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js b/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js index 5d91c634fd74e..574deffc37cd3 100644 --- a/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js +++ b/packages/react-native-editor/__device-tests__/gutenberg-editor-cover.test.js @@ -2,7 +2,12 @@ * Internal dependencies */ import EditorPage from './pages/editor-page'; -import { setupDriver, isLocalEnvironment, stopDriver } from './helpers/utils'; +import { + setupDriver, + isLocalEnvironment, + stopDriver, + isAndroid, +} from './helpers/utils'; import testData from './helpers/test-data'; jest.setTimeout( 1000000 ); @@ -33,17 +38,22 @@ describe( 'Gutenberg Editor Cover Block test', () => { await expect( editorPage.getBlockList() ).resolves.toBe( true ); } ); - it( 'should convert height properly', async () => { + it( 'should displayed properly and have properly converted height (ios only)', async () => { await editorPage.setHtmlContent( testData.coverHeightWithRemUnit ); const coverBlock = await editorPage.getBlockAtPosition( coverBlockName ); - const { height } = await coverBlock.getSize(); - // Height is set to 20rem, where 1rem is 16. - // There is also block's vertical padding equal 32. - // Finally, the total height should be 20 * 16 + 32 = 352 - expect( height ).toBe( 352 ); + + // Temporarily this test is skipped on Android,due to the inconsistency of the results, + // which are related to getting values in raw pixels instead of density pixels on Android. + if ( ! isAndroid() ) { + const { height } = await coverBlock.getSize(); + // Height is set to 20rem, where 1rem is 16. + // There is also block's vertical padding equal 32. + // Finally, the total height should be 20 * 16 + 32 = 352 + expect( height ).toBe( 352 ); + } await coverBlock.click(); expect( coverBlock ).toBeTruthy(); From bb9565ba632660a0540287b1005948abbd34d03b Mon Sep 17 00:00:00 2001 From: Kjell Reigstad Date: Mon, 23 Nov 2020 11:55:02 -0500 Subject: [PATCH 011/317] Add alignment controls to the featured image block. (#27076) --- packages/block-library/src/post-featured-image/block.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-library/src/post-featured-image/block.json b/packages/block-library/src/post-featured-image/block.json index 0c183bf7d0e23..a7ac68c85ff89 100644 --- a/packages/block-library/src/post-featured-image/block.json +++ b/packages/block-library/src/post-featured-image/block.json @@ -13,6 +13,7 @@ "postType" ], "supports": { + "align": [ "left", "right", "center", "wide", "full" ], "html": false } } From d44a08240d47b24267e00a33ac00831cb3e792aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1vid=20Szab=C3=B3?= Date: Mon, 23 Nov 2020 18:51:21 +0100 Subject: [PATCH 012/317] Navigation panel: Hide empty menus (#27141) Hides empty menus in the navigation panel. Namely Templates -> Pages and Templates -> Posts. --- .../navigation-panel/menus/templates-pages.js | 6 +++--- .../navigation-panel/menus/templates-posts.js | 14 ++++++++------ .../navigation-panel/menus/templates.js | 2 ++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-pages.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-pages.js index 1b000d83e12f0..d4816219a0239 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-pages.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-pages.js @@ -20,15 +20,15 @@ import { MENU_TEMPLATES, MENU_TEMPLATES_PAGES } from '../constants'; export default function TemplatesPagesMenu( { templates } ) { const defaultTemplate = templates?.find( ( { slug } ) => slug === 'page' ); - const specificTemplates = templates?.filter( ( { slug } ) => - slug.startsWith( 'page-' ) - ); + const specificTemplates = + templates?.filter( ( { slug } ) => slug.startsWith( 'page-' ) ) ?? []; return ( { map( specificTemplates, ( template ) => ( diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-posts.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-posts.js index 5af56fe7a58d0..02225ee5bfb24 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-posts.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates-posts.js @@ -23,18 +23,20 @@ import { } from '../constants'; export default function TemplatesPostsMenu( { templates } ) { - const generalTemplates = templates?.filter( ( { slug } ) => - TEMPLATES_POSTS.includes( slug ) - ); - const specificTemplates = templates?.filter( ( { slug } ) => - slug.startsWith( 'post-' ) - ); + const generalTemplates = + templates?.filter( ( { slug } ) => TEMPLATES_POSTS.includes( slug ) ) ?? + []; + const specificTemplates = + templates?.filter( ( { slug } ) => slug.startsWith( 'post-' ) ) ?? []; return ( { map( specificTemplates, ( template ) => ( diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js index f1f188b6d7a3b..e96aaef7f8d99 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js @@ -65,10 +65,12 @@ export default function TemplatesMenu() { From c361b6386850eec0de9cf486b0cd8cde6f772df2 Mon Sep 17 00:00:00 2001 From: Jacopo Tomasone Date: Mon, 23 Nov 2020 19:33:15 +0000 Subject: [PATCH 013/317] Full Site Editing: Update the template parts admin list with new columns and views (#27156) * Show the same data in both wp_template and wp_template_part admin lists * Move the templates admin lists hooks in a separate utilities file --- lib/full-site-editing/templates-utils.php | 118 ++++++++++++++++++++++ lib/load.php | 1 + lib/template-parts.php | 36 +------ lib/templates.php | 115 +-------------------- 4 files changed, 127 insertions(+), 143 deletions(-) create mode 100644 lib/full-site-editing/templates-utils.php diff --git a/lib/full-site-editing/templates-utils.php b/lib/full-site-editing/templates-utils.php new file mode 100644 index 0000000000000..62fb7f6da83bb --- /dev/null +++ b/lib/full-site-editing/templates-utils.php @@ -0,0 +1,118 @@ +post_name ); + return; + } + + if ( 'description' === $column_name && has_excerpt( $post_id ) ) { + the_excerpt( $post_id ); + return; + } + + if ( 'status' === $column_name ) { + $post_status = get_post_status( $post_id ); + // The auto-draft status doesn't have localized labels. + if ( 'auto-draft' === $post_status ) { + echo esc_html_x( 'Auto-Draft', 'Post status', 'gutenberg' ); + return; + } + $post_status_object = get_post_status_object( $post_status ); + echo esc_html( $post_status_object->label ); + return; + } + + if ( 'theme' === $column_name ) { + $terms = get_the_terms( $post_id, 'wp_theme' ); + $themes = array(); + $is_file_based = false; + foreach ( $terms as $term ) { + if ( '_wp_file_based' === $term->slug ) { + $is_file_based = true; + } else { + $themes[] = esc_html( wp_get_theme( $term->slug ) ); + } + } + echo implode( '
', $themes ); + if ( $is_file_based ) { + echo '
' . __( '(Created from a template file)', 'gutenberg' ); + } + return; + } +} + +/** + * Adds the auto-draft view to the templates and template parts admin lists. + * + * @param array $views The edit views to filter. + */ +function gutenberg_filter_templates_edit_views( $views ) { + $post_type = get_current_screen()->post_type; + $url = add_query_arg( + array( + 'post_type' => $post_type, + 'post_status' => 'auto-draft', + ), + 'edit.php' + ); + $is_auto_draft_view = isset( $_REQUEST['post_status'] ) && 'auto-draft' === $_REQUEST['post_status']; + $class_html = $is_auto_draft_view ? ' class="current"' : ''; + $aria_current = $is_auto_draft_view ? ' aria-current="page"' : ''; + $post_count = wp_count_posts( $post_type, 'readable' ); + $label = sprintf( + // The auto-draft status doesn't have localized labels. + translate_nooped_plural( + /* translators: %s: Number of auto-draft posts. */ + _nx_noop( + 'Auto-Draft (%s)', + 'Auto-Drafts (%s)', + 'Post status', + 'gutenberg' + ), + $post_count->{'auto-draft'} + ), + number_format_i18n( $post_count->{'auto-draft'} ) + ); + + $auto_draft_view = sprintf( + '%s', + esc_url( $url ), + $class_html, + $aria_current, + $label + ); + + array_splice( $views, 1, 0, array( 'auto-draft' => $auto_draft_view ) ); + + return $views; +} diff --git a/lib/load.php b/lib/load.php index d8b89f7ba0200..9227918378a0f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -106,6 +106,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/full-site-editing.php'; require __DIR__ . '/full-site-editing/default-template-types.php'; +require __DIR__ . '/full-site-editing/templates-utils.php'; require __DIR__ . '/templates-sync.php'; require __DIR__ . '/templates.php'; require __DIR__ . '/template-parts.php'; diff --git a/lib/template-parts.php b/lib/template-parts.php index 11532570786a5..464200f66d000 100644 --- a/lib/template-parts.php +++ b/lib/template-parts.php @@ -49,9 +49,9 @@ function gutenberg_register_template_part_post_type() { 'supports' => array( 'title', 'slug', + 'excerpt', 'editor', 'revisions', - 'custom-fields', ), ); @@ -104,36 +104,10 @@ function gutenberg_fix_template_part_admin_menu_entry() { } add_action( 'admin_menu', 'gutenberg_fix_template_part_admin_menu_entry' ); -/** - * Filters the 'wp_template_part' post type columns in the admin list table. - * - * @param array $columns Columns to display. - * @return array Filtered $columns. - */ -function gutenberg_filter_template_part_list_table_columns( array $columns ) { - $columns['slug'] = __( 'Slug', 'gutenberg' ); - if ( isset( $columns['date'] ) ) { - unset( $columns['date'] ); - } - return $columns; -} -add_filter( 'manage_wp_template_part_posts_columns', 'gutenberg_filter_template_part_list_table_columns' ); - -/** - * Renders column content for the 'wp_template_part' post type list table. - * - * @param string $column_name Column name to render. - * @param int $post_id Post ID. - */ -function gutenberg_render_template_part_list_table_column( $column_name, $post_id ) { - if ( 'slug' !== $column_name ) { - return; - } - $post = get_post( $post_id ); - echo esc_html( $post->post_name ); -} -add_action( 'manage_wp_template_part_posts_custom_column', 'gutenberg_render_template_part_list_table_column', 10, 2 ); - +// Customize the `wp_template` admin list. +add_filter( 'manage_wp_template_part_posts_columns', 'gutenberg_templates_lists_custom_columns' ); +add_action( 'manage_wp_template_part_posts_custom_column', 'gutenberg_render_templates_lists_custom_column', 10, 2 ); +add_filter( 'views_edit-wp_template_part', 'gutenberg_filter_templates_edit_views' ); /** * Filter for adding and a `theme` parameter to `wp_template_part` queries. diff --git a/lib/templates.php b/lib/templates.php index d99d146e9eff4..aa085c0d54d12 100644 --- a/lib/templates.php +++ b/lib/templates.php @@ -192,118 +192,9 @@ function gutenberg_fix_template_admin_menu_entry() { } add_action( 'admin_menu', 'gutenberg_fix_template_admin_menu_entry' ); -/** - * Filters the 'wp_template' post type columns in the admin list table. - * - * @param array $columns Columns to display. - * @return array Filtered $columns. - */ -function gutenberg_filter_template_list_table_columns( array $columns ) { - $columns['slug'] = __( 'Slug', 'gutenberg' ); - $columns['description'] = __( 'Description', 'gutenberg' ); - $columns['status'] = __( 'Status', 'gutenberg' ); - $columns['theme'] = __( 'Theme', 'gutenberg' ); - if ( isset( $columns['date'] ) ) { - unset( $columns['date'] ); - } - return $columns; -} -add_filter( 'manage_wp_template_posts_columns', 'gutenberg_filter_template_list_table_columns' ); - -/** - * Renders column content for the 'wp_template' post type list table. - * - * @param string $column_name Column name to render. - * @param int $post_id Post ID. - */ -function gutenberg_render_template_list_table_column( $column_name, $post_id ) { - if ( 'slug' === $column_name ) { - $post = get_post( $post_id ); - echo esc_html( $post->post_name ); - return; - } - - if ( 'description' === $column_name ) { - the_excerpt( $post_id ); - return; - } - - if ( 'status' === $column_name ) { - $post_status = get_post_status( $post_id ); - // The auto-draft status doesn't have localized labels. - if ( 'auto-draft' === $post_status ) { - echo esc_html_x( 'Auto-Draft', 'Post status', 'gutenberg' ); - return; - } - $post_status_object = get_post_status_object( $post_status ); - echo esc_html( $post_status_object->label ); - return; - } - - if ( 'theme' === $column_name ) { - $terms = get_the_terms( $post_id, 'wp_theme' ); - $themes = array(); - $is_file_based = false; - foreach ( $terms as $term ) { - if ( '_wp_file_based' === $term->slug ) { - $is_file_based = true; - } else { - $themes[] = esc_html( wp_get_theme( $term->slug ) ); - } - } - echo implode( '
', $themes ); - if ( $is_file_based ) { - echo '
' . __( '(Created from a template file)', 'gutenberg' ); - } - return; - } -} -add_action( 'manage_wp_template_posts_custom_column', 'gutenberg_render_template_list_table_column', 10, 2 ); - -/** - * Adds the auto-draft view to the 'wp_template' post type list. - * - * @param array $views The edit views to filter. - */ -function gutenberg_filter_templates_edit_views( $views ) { - $url = add_query_arg( - array( - 'post_type' => 'wp_template', - 'post_status' => 'auto-draft', - ), - 'edit.php' - ); - $is_auto_draft_view = isset( $_REQUEST['post_status'] ) && 'auto-draft' === $_REQUEST['post_status']; - $class_html = $is_auto_draft_view ? ' class="current"' : ''; - $aria_current = $is_auto_draft_view ? ' aria-current="page"' : ''; - $post_count = wp_count_posts( 'wp_template', 'readable' ); - $label = sprintf( - // The auto-draft status doesn't have localized labels. - translate_nooped_plural( - /* translators: %s: Number of auto-draft posts. */ - _nx_noop( - 'Auto-Draft (%s)', - 'Auto-Drafts (%s)', - 'Post status', - 'gutenberg' - ), - $post_count->{'auto-draft'} - ), - number_format_i18n( $post_count->{'auto-draft'} ) - ); - - $auto_draft_view = sprintf( - '%s', - esc_url( $url ), - $class_html, - $aria_current, - $label - ); - - array_splice( $views, 1, 0, array( 'auto-draft' => $auto_draft_view ) ); - - return $views; -} +// Customize the `wp_template` admin list. +add_filter( 'manage_wp_template_posts_columns', 'gutenberg_templates_lists_custom_columns' ); +add_action( 'manage_wp_template_posts_custom_column', 'gutenberg_render_templates_lists_custom_column', 10, 2 ); add_filter( 'views_edit-wp_template', 'gutenberg_filter_templates_edit_views' ); /** From df2e65b3a352d94486f18d484c04a31cfd824470 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Tue, 24 Nov 2020 12:25:26 +1000 Subject: [PATCH 014/317] Block Support: Separate opt in for font style and weight options (#26844) Updates the recently added font appearance block support to separate the style and weight aspects in terms of opting in via block.json. This required: * Updating the detection of support opt in within typography tools * Updating the UI control to handle the situation when only one option is opted into This change also includes adding translation for the combined UI option labels and extracting concerns of how the selections are stored within style attributes from the UI control. --- .../developers/themes/theme-json.md | 1 + lib/block-supports/typography.php | 21 ++- lib/class-wp-theme-json.php | 8 +- lib/experimental-default-theme.json | 33 +++-- lib/global-styles.php | 2 +- .../font-appearance-control/index.js | 121 ++++++++++++------ .../block-editor/src/hooks/font-appearance.js | 116 ++++++++++++++--- packages/block-editor/src/hooks/typography.js | 6 +- .../block-library/src/navigation/block.json | 3 +- packages/blocks/src/api/constants.js | 4 +- 10 files changed, 232 insertions(+), 83 deletions(-) diff --git a/docs/designers-developers/developers/themes/theme-json.md b/docs/designers-developers/developers/themes/theme-json.md index 86fbfc115716a..db4fd09787fba 100644 --- a/docs/designers-developers/developers/themes/theme-json.md +++ b/docs/designers-developers/developers/themes/theme-json.md @@ -97,6 +97,7 @@ The settings section has the following structure and default values: "dropCap": true, /* false to opt-out */ "fontFamilies": [ ... ], /* font family presets */ "fontSizes": [ ... ], /* font size presets, as in add_theme_support('editor-font-sizes', ... ) */ + "fontStyles": [ ... ], /* font style presets */ "fontWeights": [ ... ], /* font weight presets */ "textDecorations": [ ... ], /* text decoration presets */ "textTransforms": [ ... ] /* text transform presets */ diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index d797bdc21bac4..29454c43296fd 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -15,12 +15,19 @@ function gutenberg_register_typography_support( $block_type ) { return; } - $has_font_appearance_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontAppearance' ), false ); $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); + $has_font_style_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontStyle' ), false ); + $has_font_weight_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontWeight' ), false ); $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); $has_text_decoration_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextDecoration' ), false ); $has_text_transform_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextTransform' ), false ); - $has_typography_support = $has_font_appearance_support || $has_font_size_support || $has_line_height_support || $has_text_transform_support || $has_text_decoration_support; + + $has_typography_support = $has_font_size_support + || $has_font_weight_support + || $has_font_style_support + || $has_line_height_support + || $has_text_transform_support + || $has_text_decoration_support; if ( ! $block_type->attributes ) { $block_type->attributes = array(); @@ -57,8 +64,9 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { $classes = array(); $styles = array(); - $has_font_appearance_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontAppearance' ), false ); $has_font_family_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontFamily' ), false ); + $has_font_style_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontStyle' ), false ); + $has_font_weight_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalFontWeight' ), false ); $has_font_size_support = gutenberg_experimental_get( $block_type->supports, array( 'fontSize' ), false ); $has_line_height_support = gutenberg_experimental_get( $block_type->supports, array( 'lineHeight' ), false ); $has_text_decoration_support = gutenberg_experimental_get( $block_type->supports, array( '__experimentalTextDecoration' ), false ); @@ -94,14 +102,17 @@ function gutenberg_apply_typography_support( $block_type, $block_attributes ) { } } - // Font appearance - style and weight. - if ( $has_font_appearance_support ) { + // Font style. + if ( $has_font_style_support ) { // Apply font style. $font_style = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontStyle', 'font-style' ); if ( $font_style ) { $styles[] = $font_style; } + } + // Font weight. + if ( $has_font_weight_support ) { // Apply font weight. $font_weight = gutenberg_typography_get_css_variable_inline_style( $block_attributes, 'fontWeight', 'font-weight' ); if ( $font_weight ) { diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index 0452510d306c6..b8870b7263b40 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -221,7 +221,7 @@ class WP_Theme_JSON { ), array( 'path' => array( 'settings', 'typography', 'fontStyles' ), - 'value_key' => 'slug', + 'value_key' => 'value', 'css_var_infix' => 'font-style', 'classes' => array( array( @@ -232,7 +232,7 @@ class WP_Theme_JSON { ), array( 'path' => array( 'settings', 'typography', 'fontWeights' ), - 'value_key' => 'slug', + 'value_key' => 'value', 'css_var_infix' => 'font-weight', 'classes' => array( array( @@ -300,11 +300,11 @@ class WP_Theme_JSON { ), 'fontStyle' => array( 'value' => array( 'typography', 'fontStyle' ), - 'support' => array( '__experimentalFontAppearance' ), + 'support' => array( '__experimentalFontStyle' ), ), 'fontWeight' => array( 'value' => array( 'typography', 'fontWeight' ), - 'support' => array( '__experimentalFontAppearance' ), + 'support' => array( '__experimentalFontWeight' ), ), 'lineHeight' => array( 'value' => array( 'typography', 'lineHeight' ), diff --git a/lib/experimental-default-theme.json b/lib/experimental-default-theme.json index 7b35275d9af03..82c1bccf28e73 100644 --- a/lib/experimental-default-theme.json +++ b/lib/experimental-default-theme.json @@ -164,49 +164,60 @@ "fontStyles": [ { "name": "Regular", - "slug": "normal" + "slug": "normal", + "value": "normal" }, { "name": "Italic", - "slug": "italic" + "slug": "italic", + "value": "italic" } ], "fontWeights": [ { "name": "Ultralight", - "slug": "100" + "slug": "100", + "value": "100" }, { "name": "Thin", - "slug": "200" + "slug": "200", + "value": "200" }, { "name": "Light", - "slug": "300" + "slug": "300", + "value": "300" }, { "name": "Regular", - "slug": "400" + "slug": "400", + "value": "400" }, { "name": "Medium", - "slug": "500" + "slug": "500", + "value": "500" }, { "name": "Semibold", - "slug": "600" + "slug": "600", + "value": "600" }, { "name": "Bold", - "slug": "700" + "slug": "700", + "value": "700" }, { "name": "Heavy", - "slug": "800" + "slug": "800", + "value": "800" }, { "name": "Black", - "slug": "900" + "slug": "900", + "value": "900" } ], "textTransforms": [ diff --git a/lib/global-styles.php b/lib/global-styles.php index 9ec1d698da1ff..b1aeb724588e1 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -386,7 +386,7 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree ) { if ( $can_use_cached ) { // Cache for a minute. - // This cache doesn't need to be any longer, we only want to avoid spikes on high-trafic sites. + // This cache doesn't need to be any longer, we only want to avoid spikes on high-traffic sites. set_transient( 'global_styles', $stylesheet, MINUTE_IN_SECONDS ); } diff --git a/packages/block-editor/src/components/font-appearance-control/index.js b/packages/block-editor/src/components/font-appearance-control/index.js index 29a6036d4a385..c6670368a0392 100644 --- a/packages/block-editor/src/components/font-appearance-control/index.js +++ b/packages/block-editor/src/components/font-appearance-control/index.js @@ -3,72 +3,121 @@ */ import { CustomSelectControl } from '@wordpress/components'; import { useMemo } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Control to display unified font style and weight options. * - * @param {Object} props Component props. - * @param {Object} props.value Currently selected combination of font style and weight. - * @param {Object} props.options Object containing weight and style options. - * @param {Function} props.onChange Handles selection change. - * @return {WPElement} Font appearance control. + * @param {Object} props Component props. + * @return {WPElement} Font appearance control. */ -export default function FontAppearanceControl( { value, options, onChange } ) { - const { fontStyle, fontWeight } = value; - const { fontStyles = [], fontWeights = [] } = options; - const hasStylesOrWeights = fontStyles.length > 0 || fontWeights.length > 0; +export default function FontAppearanceControl( props ) { + const { + onChange, + options: { fontStyles = [], fontWeights = [] }, + value: { fontStyle, fontWeight }, + } = props; + const hasStyles = !! fontStyles.length; + const hasWeights = !! fontWeights.length; + const hasStylesOrWeights = hasStyles || hasWeights; + const defaultOption = { + key: 'default', + name: __( 'Default' ), + style: { fontStyle: undefined, fontWeight: undefined }, + }; - // Map font styles and weights to select options. - const selectOptions = useMemo( () => { - const defaultCombo = { fontStyle: undefined, fontWeight: undefined }; - const combinedOptions = [ - { - key: 'default', - name: __( 'Default' ), - style: defaultCombo, - presetStyle: defaultCombo, - }, - ]; + // Combines both font style and weight options into a single dropdown. + const combineOptions = () => { + const combinedOptions = [ defaultOption ]; fontStyles.forEach( ( { name: styleName, slug: styleSlug } ) => { fontWeights.forEach( ( { name: weightName, slug: weightSlug } ) => { + const optionName = + styleSlug === 'normal' + ? weightName + : sprintf( + /* translators: 1: Font weight name. 2: Font style name. */ + __( '%1$s %2$s' ), + weightName, + styleName + ); + combinedOptions.push( { key: `${ weightSlug }-${ styleSlug }`, - name: - styleSlug === 'normal' - ? weightName - : `${ weightName } ${ styleName }`, - // style applies font appearance to the individual select option. + name: optionName, style: { fontStyle: styleSlug, fontWeight: weightSlug }, - // presetStyle are the actual typography styles that should be given to onChange. - presetStyle: { - fontStyle: `var:preset|font-style|${ styleSlug }`, - fontWeight: `var:preset|font-weight|${ weightSlug }`, - }, } ); } ); } ); return combinedOptions; - }, [ options ] ); + }; + + // Generates select options for font styles only. + const styleOptions = () => { + const combinedOptions = [ defaultOption ]; + fontStyles.forEach( ( { name, slug } ) => { + combinedOptions.push( { + key: slug, + name, + style: { fontStyle: slug, fontWeight: undefined }, + } ); + } ); + return combinedOptions; + }; + + // Generates select options for font weights only. + const weightOptions = () => { + const combinedOptions = [ defaultOption ]; + fontWeights.forEach( ( { name, slug } ) => { + combinedOptions.push( { + key: slug, + name, + style: { fontStyle: undefined, fontWeight: slug }, + } ); + } ); + return combinedOptions; + }; + + // Map font styles and weights to select options. + const selectOptions = useMemo( () => { + if ( hasStyles && hasWeights ) { + return combineOptions(); + } + return hasStyles ? styleOptions() : weightOptions(); + }, [ props.options ] ); + + // Find current selection by comparing font style & weight against options. const currentSelection = selectOptions.find( ( option ) => - option.presetStyle.fontStyle === fontStyle && - option.presetStyle.fontWeight === fontWeight + option.style.fontStyle === fontStyle && + option.style.fontWeight === fontWeight ); + // Adjusts field label in case either styles or weights are disabled. + const getLabel = () => { + if ( ! hasStyles ) { + return __( 'Font weight' ); + } + + if ( ! hasWeights ) { + return __( 'Font style' ); + } + + return __( 'Appearance' ); + }; + return (
{ hasStylesOrWeights && ( - onChange( selectedItem.presetStyle ) + onChange( selectedItem.style ) } /> ) } diff --git a/packages/block-editor/src/hooks/font-appearance.js b/packages/block-editor/src/hooks/font-appearance.js index bbad86a20f133..e205564931637 100644 --- a/packages/block-editor/src/hooks/font-appearance.js +++ b/packages/block-editor/src/hooks/font-appearance.js @@ -11,10 +11,14 @@ import useEditorFeature from '../components/use-editor-feature'; import { cleanEmptyObject } from './utils'; /** - * Key within block settings' support array indicating support for font - * appearance options e.g. font weight and style. + * Key within block settings' support array indicating support for font style. */ -export const FONT_APPEARANCE_SUPPORT_KEY = '__experimentalFontAppearance'; +export const FONT_STYLE_SUPPORT_KEY = '__experimentalFontStyle'; + +/** + * Key within block settings' support array indicating support for font weight. + */ +export const FONT_WEIGHT_SUPPORT_KEY = '__experimentalFontWeight'; /** * Inspector control panel containing the font appearance options. @@ -30,53 +34,123 @@ export function FontAppearanceEdit( props ) { const fontStyles = useEditorFeature( 'typography.fontStyles' ); const fontWeights = useEditorFeature( 'typography.fontWeights' ); - const isDisabled = useIsFontAppearanceDisabled( props ); + const isFontStyleDisabled = useIsFontStyleDisabled( props ); + const isFontWeightDisabled = useIsFontWeightDisabled( props ); - if ( isDisabled ) { + if ( isFontStyleDisabled && isFontWeightDisabled ) { return null; } const onChange = ( newStyles ) => { + // Match style selection with preset and create CSS var style if appropriate. + const presetStyle = fontStyles.find( + ( { slug } ) => slug === newStyles.fontStyle + ); + const newFontStyle = presetStyle + ? `var:preset|font-style|${ presetStyle.slug }` + : undefined; + + // Match weight selection with preset and create CSS var style if appropriate. + const presetWeight = fontWeights.find( + ( { slug } ) => slug === newStyles.fontWeight + ); + const newFontWeight = presetWeight + ? `var:preset|font-weight|${ presetWeight.slug }` + : undefined; + setAttributes( { style: cleanEmptyObject( { ...style, typography: { ...style?.typography, - ...newStyles, + fontStyle: newFontStyle, + fontWeight: newFontWeight, }, } ), } ); }; - const currentSelection = { - fontStyle: style?.typography?.fontStyle, - fontWeight: style?.typography?.fontWeight, - }; + const fontStyle = getFontAppearanceValueFromStyle( + fontStyles, + style?.typography?.fontStyle + ); + + const fontWeight = getFontAppearanceValueFromStyle( + fontWeights, + style?.typography?.fontWeight + ); return ( ); } /** - * Checks if font appearance support has been disabled. + * Checks if font style support has been disabled either by not opting in for + * support or by failing to provide preset styles. * * @param {Object} props Block properties. * @param {string} props.name Name for the block type. - * @return {boolean} Whether font appearance support has been disabled. + * @return {boolean} Whether font style support has been disabled. */ -export function useIsFontAppearanceDisabled( { name: blockName } = {} ) { - const notSupported = ! hasBlockSupport( - blockName, - FONT_APPEARANCE_SUPPORT_KEY - ); +export function useIsFontStyleDisabled( { name: blockName } = {} ) { + const styleSupport = hasBlockSupport( blockName, FONT_STYLE_SUPPORT_KEY ); const fontStyles = useEditorFeature( 'typography.fontStyles' ); + + return ! styleSupport || ! fontStyles?.length; +} + +/** + * Checks if font weight support has been disabled either by not opting in for + * support or by failing to provide preset weights. + * + * @param {Object} props Block properties. + * @param {string} props.name Name for the block type. + * @return {boolean} Whether font weight support has been disabled. + */ +export function useIsFontWeightDisabled( { name: blockName } = {} ) { + const weightSupport = hasBlockSupport( blockName, FONT_WEIGHT_SUPPORT_KEY ); const fontWeights = useEditorFeature( 'typography.fontWeights' ); - const hasFontAppearance = !! fontStyles?.length && !! fontWeights?.length; - return notSupported || ! hasFontAppearance; + return ! weightSupport || ! fontWeights?.length; +} + +/** + * Checks if font appearance support has been disabled. + * + * @param {Object} props Block properties. + * @return {boolean} Whether font appearance support has been disabled. + */ +export function useIsFontAppearanceDisabled( props ) { + const stylesDisabled = useIsFontStyleDisabled( props ); + const weightsDisabled = useIsFontWeightDisabled( props ); + + return stylesDisabled && weightsDisabled; } + +/** + * Extracts the current selection, if available, from the CSS variable set + * within a style attribute property e.g. `style.typography.fontStyle` + * or `style.typography.fontWeight`. + * + * @param {Array} presets Available preset options. + * @param {string} style Style attribute value to parse + * @return {string} Actual CSS property value. + */ +const getFontAppearanceValueFromStyle = ( presets, style ) => { + if ( ! style ) { + return undefined; + } + + const parsedValue = style.slice( style.lastIndexOf( '|' ) + 1 ); + const preset = presets.find( ( { slug } ) => slug === parsedValue ); + + return preset?.slug || style; +}; diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index db17bd98aaca9..dfc27748acb00 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -18,7 +18,8 @@ import { useIsLineHeightDisabled, } from './line-height'; import { - FONT_APPEARANCE_SUPPORT_KEY, + FONT_STYLE_SUPPORT_KEY, + FONT_WEIGHT_SUPPORT_KEY, FontAppearanceEdit, useIsFontAppearanceDisabled, } from './font-appearance'; @@ -43,8 +44,9 @@ import { export const TYPOGRAPHY_SUPPORT_KEYS = [ LINE_HEIGHT_SUPPORT_KEY, - FONT_APPEARANCE_SUPPORT_KEY, FONT_SIZE_SUPPORT_KEY, + FONT_STYLE_SUPPORT_KEY, + FONT_WEIGHT_SUPPORT_KEY, FONT_FAMILY_SUPPORT_KEY, TEXT_DECORATION_SUPPORT_KEY, TEXT_TRANSFORM_SUPPORT_KEY, diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 66fa697b3eacd..56a586f1aac25 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -50,7 +50,8 @@ "html": false, "inserter": true, "fontSize": true, - "__experimentalFontAppearance": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, "__experimentalTextTransform": true, "color": true, "__experimentalFontFamily": true, diff --git a/packages/blocks/src/api/constants.js b/packages/blocks/src/api/constants.js index 1dee92f5b47b0..68a08552f16b0 100644 --- a/packages/blocks/src/api/constants.js +++ b/packages/blocks/src/api/constants.js @@ -39,11 +39,11 @@ export const __EXPERIMENTAL_STYLE_PROPERTY = { }, fontStyle: { value: [ 'typography', 'fontStyle' ], - support: [ '__experimentalFontAppearance' ], + support: [ '__experimentalFontStyle' ], }, fontWeight: { value: [ 'typography', 'fontWeight' ], - support: [ '__experimentalFontAppearance' ], + support: [ '__experimentalFontWeight' ], }, lineHeight: { value: [ 'typography', 'lineHeight' ], From 3067e91f1ceb81b7a678bd9c4bd64835021f7012 Mon Sep 17 00:00:00 2001 From: Rafael Galani Date: Tue, 24 Nov 2020 08:29:43 -0300 Subject: [PATCH 015/317] Replace "core/block-directory" store name string with exposed store definition (#27178) * Replace store name string with store definition * Triggering build --- .../src/components/auto-block-uninstaller/index.js | 9 +++++++-- .../src/components/downloadable-block-list-item/index.js | 3 ++- .../src/components/downloadable-block-notice/index.js | 7 ++++++- .../src/components/downloadable-blocks-list/index.js | 3 ++- .../src/components/downloadable-blocks-panel/index.js | 3 ++- .../src/plugins/get-install-missing/index.js | 3 ++- .../src/plugins/get-install-missing/install-button.js | 9 +++++++-- .../plugins/installed-blocks-pre-publish-panel/index.js | 3 ++- 8 files changed, 30 insertions(+), 10 deletions(-) diff --git a/packages/block-directory/src/components/auto-block-uninstaller/index.js b/packages/block-directory/src/components/auto-block-uninstaller/index.js index 12016417bc885..e35141c5e0858 100644 --- a/packages/block-directory/src/components/auto-block-uninstaller/index.js +++ b/packages/block-directory/src/components/auto-block-uninstaller/index.js @@ -5,8 +5,13 @@ import { unregisterBlockType } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { store as blockDirectoryStore } from '../../store'; + export default function AutoBlockUninstaller() { - const { uninstallBlockType } = useDispatch( 'core/block-directory' ); + const { uninstallBlockType } = useDispatch( blockDirectoryStore ); const shouldRemoveBlockTypes = useSelect( ( select ) => { const { isAutosavingPost, isSavingPost } = select( 'core/editor' ); @@ -14,7 +19,7 @@ export default function AutoBlockUninstaller() { }, [] ); const unusedBlockTypes = useSelect( - ( select ) => select( 'core/block-directory' ).getUnusedBlockTypes(), + ( select ) => select( blockDirectoryStore ).getUnusedBlockTypes(), [] ); diff --git a/packages/block-directory/src/components/downloadable-block-list-item/index.js b/packages/block-directory/src/components/downloadable-block-list-item/index.js index 5d35741af5138..8a88e8593f0ad 100644 --- a/packages/block-directory/src/components/downloadable-block-list-item/index.js +++ b/packages/block-directory/src/components/downloadable-block-list-item/index.js @@ -10,12 +10,13 @@ import DownloadableBlockAuthorInfo from '../downloadable-block-author-info'; import DownloadableBlockHeader from '../downloadable-block-header'; import DownloadableBlockInfo from '../downloadable-block-info'; import DownloadableBlockNotice from '../downloadable-block-notice'; +import { store as blockDirectoryStore } from '../../store'; export default function DownloadableBlockListItem( { item, onClick } ) { const { isLoading, isInstallable } = useSelect( ( select ) => { const { isInstalling, getErrorNoticeForBlock } = select( - 'core/block-directory' + blockDirectoryStore ); const notice = getErrorNoticeForBlock( item.id ); const hasFatal = notice && notice.isFatal; diff --git a/packages/block-directory/src/components/downloadable-block-notice/index.js b/packages/block-directory/src/components/downloadable-block-notice/index.js index 75e7d5a513bca..f24545954e63a 100644 --- a/packages/block-directory/src/components/downloadable-block-notice/index.js +++ b/packages/block-directory/src/components/downloadable-block-notice/index.js @@ -5,10 +5,15 @@ import { __ } from '@wordpress/i18n'; import { Button, Notice } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as blockDirectoryStore } from '../../store'; + export const DownloadableBlockNotice = ( { block, onClick } ) => { const errorNotice = useSelect( ( select ) => - select( 'core/block-directory' ).getErrorNoticeForBlock( block.id ), + select( blockDirectoryStore ).getErrorNoticeForBlock( block.id ), [ block ] ); diff --git a/packages/block-directory/src/components/downloadable-blocks-list/index.js b/packages/block-directory/src/components/downloadable-blocks-list/index.js index 5aff92c1914a1..d732b7bc800d5 100644 --- a/packages/block-directory/src/components/downloadable-blocks-list/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-list/index.js @@ -12,9 +12,10 @@ import { useDispatch } from '@wordpress/data'; * Internal dependencies */ import DownloadableBlockListItem from '../downloadable-block-list-item'; +import { store as blockDirectoryStore } from '../../store'; function DownloadableBlocksList( { items, onHover = noop, onSelect } ) { - const { installBlockType } = useDispatch( 'core/block-directory' ); + const { installBlockType } = useDispatch( blockDirectoryStore ); const { setIsInserterOpened } = useDispatch( 'core/edit-post' ); if ( ! items.length ) { diff --git a/packages/block-directory/src/components/downloadable-blocks-panel/index.js b/packages/block-directory/src/components/downloadable-blocks-panel/index.js index 6cdde9efc28c9..fa6af8b174dbd 100644 --- a/packages/block-directory/src/components/downloadable-blocks-panel/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-panel/index.js @@ -12,6 +12,7 @@ import { speak } from '@wordpress/a11y'; * Internal dependencies */ import DownloadableBlocksList from '../downloadable-blocks-list'; +import { store as blockDirectoryStore } from '../../store'; function DownloadableBlocksPanel( { downloadableItems, @@ -80,7 +81,7 @@ export default compose( [ const { getDownloadableBlocks, isRequestingDownloadableBlocks, - } = select( 'core/block-directory' ); + } = select( blockDirectoryStore ); const hasPermission = select( 'core' ).canUser( 'read', diff --git a/packages/block-directory/src/plugins/get-install-missing/index.js b/packages/block-directory/src/plugins/get-install-missing/index.js index da4337e69249c..563fd6e845b58 100644 --- a/packages/block-directory/src/plugins/get-install-missing/index.js +++ b/packages/block-directory/src/plugins/get-install-missing/index.js @@ -12,6 +12,7 @@ import { Warning } from '@wordpress/block-editor'; * Internal dependencies */ import InstallButton from './install-button'; +import { store as blockDirectoryStore } from '../../store'; const getInstallMissing = ( OriginalComponent ) => ( props ) => { const { originalName, originalUndelimitedContent } = props.attributes; @@ -19,7 +20,7 @@ const getInstallMissing = ( OriginalComponent ) => ( props ) => { // eslint-disable-next-line react-hooks/rules-of-hooks const { block, hasPermission } = useSelect( ( select ) => { - const { getDownloadableBlocks } = select( 'core/block-directory' ); + const { getDownloadableBlocks } = select( blockDirectoryStore ); const blocks = getDownloadableBlocks( 'block:' + originalName ).filter( ( { name } ) => originalName === name ); diff --git a/packages/block-directory/src/plugins/get-install-missing/install-button.js b/packages/block-directory/src/plugins/get-install-missing/install-button.js index f4985de41846e..7805589da4049 100644 --- a/packages/block-directory/src/plugins/get-install-missing/install-button.js +++ b/packages/block-directory/src/plugins/get-install-missing/install-button.js @@ -6,11 +6,16 @@ import { Button } from '@wordpress/components'; import { createBlock, getBlockType, parse } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; +/** + * Internal dependencies + */ +import { store as blockDirectoryStore } from '../../store'; + export default function InstallButton( { attributes, block, clientId } ) { const isInstallingBlock = useSelect( ( select ) => - select( 'core/block-directory' ).isInstalling( block.id ) + select( blockDirectoryStore ).isInstalling( block.id ) ); - const { installBlockType } = useDispatch( 'core/block-directory' ); + const { installBlockType } = useDispatch( blockDirectoryStore ); const { replaceBlock } = useDispatch( 'core/block-editor' ); return ( diff --git a/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js b/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js index f00528793dc63..a51b68fcaffef 100644 --- a/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js +++ b/packages/block-directory/src/plugins/installed-blocks-pre-publish-panel/index.js @@ -10,10 +10,11 @@ import { blockDefault } from '@wordpress/icons'; * Internal dependencies */ import CompactList from '../../components/compact-list'; +import { store as blockDirectoryStore } from '../../store'; export default function InstalledBlocksPrePublishPanel() { const newBlockTypes = useSelect( - ( select ) => select( 'core/block-directory' ).getNewBlockTypes(), + ( select ) => select( blockDirectoryStore ).getNewBlockTypes(), [] ); From fd4ad41e61a371ae6565b0957b1ab65ceef8809b Mon Sep 17 00:00:00 2001 From: Jacopo Tomasone Date: Tue, 24 Nov 2020 11:31:10 +0000 Subject: [PATCH 016/317] Full Site Editing: Avoid throwing warnings if there are no terms for a template or template part (#27210) --- lib/full-site-editing/templates-utils.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/full-site-editing/templates-utils.php b/lib/full-site-editing/templates-utils.php index 62fb7f6da83bb..5430470cfa640 100644 --- a/lib/full-site-editing/templates-utils.php +++ b/lib/full-site-editing/templates-utils.php @@ -53,7 +53,10 @@ function gutenberg_render_templates_lists_custom_column( $column_name, $post_id } if ( 'theme' === $column_name ) { - $terms = get_the_terms( $post_id, 'wp_theme' ); + $terms = get_the_terms( $post_id, 'wp_theme' ); + if ( empty( $terms ) || is_wp_error( $terms ) ) { + return; + } $themes = array(); $is_file_based = false; foreach ( $terms as $term ) { @@ -77,7 +80,7 @@ function gutenberg_render_templates_lists_custom_column( $column_name, $post_id * @param array $views The edit views to filter. */ function gutenberg_filter_templates_edit_views( $views ) { - $post_type = get_current_screen()->post_type; + $post_type = get_current_screen()->post_type; $url = add_query_arg( array( 'post_type' => $post_type, From 0b2989f4a227a1c359badd8d238166228bf02e3c Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 24 Nov 2020 13:40:32 +0200 Subject: [PATCH 017/317] fix image double paste (#27199) --- .../src/utils/get-paste-event-data.js | 6 +++--- packages/components/src/drop-zone/provider.js | 4 ++-- packages/dom/README.md | 2 +- packages/dom/src/data-transfer.js | 16 ++++++++++++---- packages/rich-text/src/component/index.js | 2 +- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/utils/get-paste-event-data.js b/packages/block-editor/src/utils/get-paste-event-data.js index b840de2964250..0db158c5f6e8e 100644 --- a/packages/block-editor/src/utils/get-paste-event-data.js +++ b/packages/block-editor/src/utils/get-paste-event-data.js @@ -25,9 +25,9 @@ export function getPasteEventData( { clipboardData } ) { } } - const files = [ - ...getFilesFromDataTransfer( clipboardData ), - ].filter( ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); + const files = getFilesFromDataTransfer( + clipboardData + ).filter( ( { type } ) => /^image\/(?:jpe?g|png|gif)$/.test( type ) ); // Only process files if no HTML is present. // A pasted file may have the URL as plain text. diff --git a/packages/components/src/drop-zone/provider.js b/packages/components/src/drop-zone/provider.js index 45c6954a6a9be..80faacf92102c 100644 --- a/packages/components/src/drop-zone/provider.js +++ b/packages/components/src/drop-zone/provider.js @@ -22,7 +22,7 @@ const { Provider } = Context; function getDragEventType( { dataTransfer } ) { if ( dataTransfer ) { - if ( getFilesFromDataTransfer( dataTransfer ).size > 0 ) { + if ( getFilesFromDataTransfer( dataTransfer ).length > 0 ) { return 'file'; } @@ -204,7 +204,7 @@ export default function DropZoneProvider( { children } ) { switch ( dragEventType ) { case 'file': hoveredDropZone.onFilesDrop( - [ ...getFilesFromDataTransfer( event.dataTransfer ) ], + getFilesFromDataTransfer( event.dataTransfer ), position ); break; diff --git a/packages/dom/README.md b/packages/dom/README.md index 6fe8d1ca75825..d888a810f109d 100644 --- a/packages/dom/README.md +++ b/packages/dom/README.md @@ -84,7 +84,7 @@ _Parameters_ _Returns_ -- `Set`: A set containing all files. +- `Array`: An array containing all files. # **getOffsetParent** diff --git a/packages/dom/src/data-transfer.js b/packages/dom/src/data-transfer.js index 48cd3c55d9d41..6052326c2c221 100644 --- a/packages/dom/src/data-transfer.js +++ b/packages/dom/src/data-transfer.js @@ -3,16 +3,24 @@ * * @param {DataTransfer} dataTransfer DataTransfer object to inspect. * - * @return {Set} A set containing all files. + * @return {Object[]} An array containing all files. */ export function getFilesFromDataTransfer( dataTransfer ) { - const files = new Set( dataTransfer.files ); + const files = [ ...dataTransfer.files ]; Array.from( dataTransfer.items ).forEach( ( item ) => { const file = item.getAsFile(); - if ( file ) { - files.add( file ); + if ( + file && + ! files.find( + ( { name, type, size } ) => + name === file.name && + type === file.type && + size === file.size + ) + ) { + files.push( file ); } } ); diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index e7d6b6cbb577d..625da9012fd05 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -374,7 +374,7 @@ function RichText( } if ( onPaste ) { - const files = [ ...getFilesFromDataTransfer( clipboardData ) ]; + const files = getFilesFromDataTransfer( clipboardData ); onPaste( { value: removeEditorOnlyFormats( record.current ), From e2ffa926871d502ce112aac7a325b3697b3e2ba1 Mon Sep 17 00:00:00 2001 From: Rafael Galani Date: Tue, 24 Nov 2020 08:49:58 -0300 Subject: [PATCH 018/317] Replace store name string with store definition (#27182) --- .../src/components/layout/use-navigation-block-editor.js | 3 ++- .../src/components/layout/use-navigation-editor.js | 7 ++++++- .../edit-navigation/src/components/toolbar/save-button.js | 7 ++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js b/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js index d2d5034878ff6..e9108b50bbefc 100644 --- a/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js +++ b/packages/edit-navigation/src/components/layout/use-navigation-block-editor.js @@ -9,9 +9,10 @@ import { useEntityBlockEditor } from '@wordpress/core-data'; * Internal dependencies */ import { KIND, POST_TYPE } from '../../store/utils'; +import { store as editNavigationStore } from '../../store'; export default function useNavigationBlockEditor( post ) { - const { createMissingMenuItems } = useDispatch( 'core/edit-navigation' ); + const { createMissingMenuItems } = useDispatch( editNavigationStore ); const [ blocks, onInput, _onChange ] = useEntityBlockEditor( KIND, diff --git a/packages/edit-navigation/src/components/layout/use-navigation-editor.js b/packages/edit-navigation/src/components/layout/use-navigation-editor.js index 590f66ac0df9f..bdc615828de3b 100644 --- a/packages/edit-navigation/src/components/layout/use-navigation-editor.js +++ b/packages/edit-navigation/src/components/layout/use-navigation-editor.js @@ -4,6 +4,11 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { store as editNavigationStore } from '../../store'; + export default function useNavigationEditor() { const menus = useSelect( ( select ) => select( 'core' ).getMenus( { per_page: -1 } ), @@ -20,7 +25,7 @@ export default function useNavigationEditor() { const navigationPost = useSelect( ( select ) => - select( 'core/edit-navigation' ).getNavigationPostForMenu( + select( editNavigationStore ).getNavigationPostForMenu( selectedMenuId ), [ selectedMenuId ] diff --git a/packages/edit-navigation/src/components/toolbar/save-button.js b/packages/edit-navigation/src/components/toolbar/save-button.js index c5adfef8b8ac7..92132f47198d6 100644 --- a/packages/edit-navigation/src/components/toolbar/save-button.js +++ b/packages/edit-navigation/src/components/toolbar/save-button.js @@ -5,8 +5,13 @@ import { useDispatch } from '@wordpress/data'; import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { store as editNavigationStore } from '../../store'; + export default function SaveButton( { navigationPost } ) { - const { saveNavigationPost } = useDispatch( 'core/edit-navigation' ); + const { saveNavigationPost } = useDispatch( editNavigationStore ); return (