Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Packages: Add package babel-plugin-transform-with-select #13177

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = function( api ) {
return {
presets: [ '@wordpress/babel-preset-default' ],
plugins: [
'@wordpress/babel-plugin-transform-with-select',
[
'@wordpress/babel-plugin-import-jsx-pragma',
{
Expand Down
7 changes: 7 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"@babel/traverse": "7.0.0",
"@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma",
"@wordpress/babel-plugin-makepot": "file:packages/babel-plugin-makepot",
"@wordpress/babel-plugin-transform-with-select": "file:packages/babel-plugin-transform-with-select",
"@wordpress/babel-preset-default": "file:packages/babel-preset-default",
"@wordpress/browserslist-config": "file:packages/browserslist-config",
"@wordpress/custom-templated-path-webpack-plugin": "file:packages/custom-templated-path-webpack-plugin",
Expand Down
35 changes: 35 additions & 0 deletions packages/babel-plugin-transform-with-select/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@wordpress/babel-plugin-transform-with-select",
"version": "1.0.0",
"description": "WordPress Babel transform to provide withSelect reducerKeys hint.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"babel",
"plugin"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/babel-plugin-transform-with-select/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"files": [
"build",
"build-module"
],
"main": "build/index.js",
"module": "build-module/index.js",
"dependencies": {
"@babel/runtime": "^7.0.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
},
"publishConfig": {
"access": "public"
}
}
56 changes: 56 additions & 0 deletions packages/babel-plugin-transform-with-select/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export default function( babel ) {
const { types: t } = babel;

function addNamespaceToWithSelect( path, namespace ) {
let parentPath = path;
while ( ( parentPath = parentPath.parentPath ) ) {
const { node } = parentPath;
if ( node.type !== 'CallExpression' || node.callee.name !== 'withSelect' ) {
continue;
}

let reducerKeys;
if ( node.arguments.length > 1 ) {
const reducerKeysNode = node.arguments[ 1 ];
switch ( reducerKeysNode.type ) {
case 'ArrayExpression':
reducerKeys = reducerKeysNode.elements.reduce( ( result, element ) => {
if ( element.type === 'StringLiteral' ) {
result.push( element.value );
}

return result;
}, [] );
break;
case 'StringLiteral':
reducerKeys = [ reducerKeysNode.value ];
break;
}

if ( reducerKeys.includes( namespace ) ) {
break;
}

reducerKeys.push( namespace );
reducerKeys = reducerKeys.map( ( key ) => t.stringLiteral( key ) );
const argumentPaths = parentPath.get( 'arguments' );
argumentPaths[ 1 ].replaceWith( t.arrayExpression( reducerKeys ) );
} else {
parentPath.pushContainer( 'arguments', t.stringLiteral( namespace ) );
}

break;
}
}

return {
visitor: {
CallExpression( path ) {
const { node } = path;
if ( node.callee.name === 'select' && node.arguments.length > 0 && node.arguments[ 0 ].type === 'StringLiteral' ) {
addNamespaceToWithSelect( path, node.arguments[ 0 ].value );
}
},
},
};
}
15 changes: 10 additions & 5 deletions packages/data/src/components/with-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import { RegistryConsumer } from '../registry-provider';
* Higher-order component used to inject state-derived props using registered
* selectors.
*
* @param {Function} mapSelectToProps Function called on every state change,
* expected to return object of props to
* merge with the component's own props.
* @param {Function} mapSelectToProps Function called on every
* state change, expected to
* return object of props to
* merge with the component's
* own props.
* @param {?(string|Array<string>)} reducerKeys Optional subset of reducer
* keys on which subscribe
* callback should be called.
*
* @return {Component} Enhanced component with merged state data props.
*/
const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( WrappedComponent ) => {
const withSelect = ( mapSelectToProps, reducerKeys ) => createHigherOrderComponent( ( WrappedComponent ) => {
/**
* Default merge props. A constant value is used as the fallback since it
* can be more efficiently shallow compared in case component is repeatedly
Expand Down Expand Up @@ -137,7 +142,7 @@ const withSelect = ( mapSelectToProps ) => createHigherOrderComponent( ( Wrapped
}

subscribe( registry ) {
this.unsubscribe = registry.subscribe( this.onStoreChange );
this.unsubscribe = registry.subscribe( this.onStoreChange, reducerKeys );
}

render() {
Expand Down
2 changes: 1 addition & 1 deletion packages/data/src/namespace-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default function createNamespace( key, options, registry ) {
lastState = state;

if ( hasChanged ) {
listener();
listener( key );
}
} );
};
Expand Down
74 changes: 65 additions & 9 deletions packages/data/src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* External dependencies
*/
import {
castArray,
without,
mapValues,
} from 'lodash';
Expand All @@ -12,6 +13,16 @@ import {
import createNamespace from './namespace-store.js';
import dataStore from './store';

/**
* Given an array of functions, invokes each function in the array with an
* empty argument set.
*
* @param {Function[]} fns Functions to invoke.
*/
function invokeForEach( fns ) {
fns.forEach( ( fn ) => fn() );
}

/**
* An isolated orchestrator of store registrations.
*
Expand Down Expand Up @@ -40,27 +51,72 @@ import dataStore from './store';
*/
export function createRegistry( storeConfigs = {} ) {
const stores = {};
let listeners = [];
let globalListeners = [];
const listenersByKey = {};

/**
* Global listener called for each store's update.
*
* @param {?string} reducerKey Key of reducer which changed, if provided by
* the registry implementation.
*/
function globalListener() {
listeners.forEach( ( listener ) => listener() );
function onStoreChange( reducerKey ) {
invokeForEach( globalListeners );

if ( reducerKey ) {
if ( listenersByKey[ reducerKey ] ) {
invokeForEach( listenersByKey[ reducerKey ] );
}
} else {
// For backwards compatibility with non-namespace-store, reducerKey
// is optional. If omitted, call every listenersByKey.
for ( const [ , listeners ] of Object.entries( listenersByKey ) ) {
invokeForEach( listeners );
}
}
}

/**
* Subscribe to changes to any data.
*
* @param {Function} listener Listener function.
* @param {Function} listener Listener function.
* @param {?(string|Array<string>)} reducerKeys Optional subset of reducer
* keys on which subscribe
* function should be called.
*
* @return {Function} Unsubscribe function.
* @return {Function} Unsubscribe function.
*/
const subscribe = ( listener ) => {
listeners.push( listener );
const subscribe = ( listener, reducerKeys ) => {
if ( reducerKeys ) {
// Overload to support string argument of `reducerKeys`.
reducerKeys = castArray( reducerKeys );

reducerKeys.forEach( ( reducerKey ) => {
if ( ! listenersByKey[ reducerKey ] ) {
listenersByKey[ reducerKey ] = [];
}

listenersByKey[ reducerKey ].push( listener );
} );

return () => {
reducerKeys.forEach( ( reducerKey ) => {
listenersByKey[ reducerKey ] = without(
listenersByKey[ reducerKey ],
listener
);

if ( ! listenersByKey[ reducerKey ].length ) {
delete listenersByKey[ reducerKey ];
}
} );
};
}

globalListeners.push( listener );

return () => {
listeners = without( listeners, listener );
globalListeners = without( globalListeners, listener );
};
};

Expand Down Expand Up @@ -122,7 +178,7 @@ export function createRegistry( storeConfigs = {} ) {
throw new TypeError( 'config.subscribe must be a function' );
}
stores[ key ] = config;
config.subscribe( globalListener );
config.subscribe( onStoreChange );
}

let registry = {
Expand Down