From 57fdb8fe9cd4aed07e05a76f41e5953e6b1b1221 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Mon, 23 Sep 2019 14:07:13 -0400 Subject: [PATCH] Implement PHP DI container and refactor. Also implements new Asset data interface for extendable settings passed to js. (https://github.com/woocommerce/woocommerce-blocks/pull/956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add dependency injection container for blocks * Add new Pacakge and Bootstrap classes. - Bootstrap for bootstrapping the plugin. - Package will replace `src/Package` and added as a dependency for any classes needing package info. * Introduce AssetsDataRegistry for managing asset data * refactor existing classes to use new DIC and Asset Data Registry - this is the bare minimum needed to make this pull viable. - further refactors will be done in more atomic smaller pulls for easier review. * add new settings handling and export `@woocommerce/settings` as an alias to wc.wcSettings - the export is exposed php side on the `wc-settings` handle. * Remove unnecessary concatenation * Fix typos and improve doc blocks * fix php linting issue * Use better escaping function. * improve jsdoc spacing * improve test assertion * use fully qualified class names in bootstrap * improve comment block to account for dynamic version string replace on build * handle exceptions a bit differently * correct dependency reference in webpack config * remove blank lines * fix doc block comment alignment * Various doc/grammar/spacing fixes from code review. Co-Authored-By: Albert Juhé Lluveras * improve naming, documentation and logic of filter callbacks While this is intended for sanitization/validation, the callback ultimately provides flexibility for filtering the value before returning or setting in state so `filter` is a better name for this. --- plugins/woocommerce-blocks/.eslintrc.js | 8 - .../assets/js/settings/blocks/constants.js | 19 ++ .../assets/js/settings/blocks/index.js | 37 +-- .../assets/js/settings/shared/currency.js | 16 -- .../js/settings/shared/default-constants.js | 13 + .../assets/js/settings/shared/get-setting.js | 26 ++ .../assets/js/settings/shared/index.js | 5 +- .../assets/js/settings/shared/set-setting.js | 17 ++ .../js/settings/shared/settings-init.js | 42 +++ .../js/settings/shared/test/get-setting.js | 17 ++ .../js/settings/shared/test/set-setting.js | 20 ++ plugins/woocommerce-blocks/src/Assets.php | 124 +++----- plugins/woocommerce-blocks/src/Assets/Api.php | 150 ++++++++++ .../src/Assets/AssetDataRegistry.php | 268 ++++++++++++++++++ .../Assets/BackCompatAssetDataRegistry.php | 79 ++++++ .../src/Domain/Bootstrap.php | 151 ++++++++++ .../woocommerce-blocks/src/Domain/Package.php | 95 +++++++ plugins/woocommerce-blocks/src/Package.php | 94 ++---- .../src/Registry/AbstractDependencyType.php | 60 ++++ .../src/Registry/Container.php | 104 +++++++ .../src/Registry/FactoryType.php | 27 ++ .../src/Registry/SharedType.php | 38 +++ .../tests/js/setup-globals.js | 11 +- .../tests/php/Assets/AssetDataRegistry.php | 55 ++++ .../tests/php/Bootstrap/MainFile.php | 49 ++++ .../tests/php/Domain/Package.php | 49 ++++ .../tests/php/Registry/Container.php | 78 +++++ .../tests/php/mocks/AssetDataRegistry.php | 26 ++ .../tests/php/mocks/MockTestDependency.php | 11 + plugins/woocommerce-blocks/webpack.config.js | 24 +- .../woocommerce-gutenberg-products-block.php | 47 ++- 31 files changed, 1521 insertions(+), 239 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/settings/blocks/constants.js delete mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/currency.js create mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.js create mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/get-setting.js create mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/set-setting.js create mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/settings-init.js create mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/test/get-setting.js create mode 100644 plugins/woocommerce-blocks/assets/js/settings/shared/test/set-setting.js create mode 100644 plugins/woocommerce-blocks/src/Assets/Api.php create mode 100644 plugins/woocommerce-blocks/src/Assets/AssetDataRegistry.php create mode 100644 plugins/woocommerce-blocks/src/Assets/BackCompatAssetDataRegistry.php create mode 100644 plugins/woocommerce-blocks/src/Domain/Bootstrap.php create mode 100644 plugins/woocommerce-blocks/src/Domain/Package.php create mode 100644 plugins/woocommerce-blocks/src/Registry/AbstractDependencyType.php create mode 100644 plugins/woocommerce-blocks/src/Registry/Container.php create mode 100644 plugins/woocommerce-blocks/src/Registry/FactoryType.php create mode 100644 plugins/woocommerce-blocks/src/Registry/SharedType.php create mode 100644 plugins/woocommerce-blocks/tests/php/Assets/AssetDataRegistry.php create mode 100644 plugins/woocommerce-blocks/tests/php/Bootstrap/MainFile.php create mode 100644 plugins/woocommerce-blocks/tests/php/Domain/Package.php create mode 100644 plugins/woocommerce-blocks/tests/php/Registry/Container.php create mode 100644 plugins/woocommerce-blocks/tests/php/mocks/AssetDataRegistry.php create mode 100644 plugins/woocommerce-blocks/tests/php/mocks/MockTestDependency.php diff --git a/plugins/woocommerce-blocks/.eslintrc.js b/plugins/woocommerce-blocks/.eslintrc.js index dd59ef8a0c0..abae53604fc 100644 --- a/plugins/woocommerce-blocks/.eslintrc.js +++ b/plugins/woocommerce-blocks/.eslintrc.js @@ -4,19 +4,11 @@ module.exports = { 'jest/globals': true, }, globals: { - wc_product_block_data: true, wcSettings: true, }, plugins: [ 'jest' ], rules: { '@wordpress/dependency-group': 'off', - camelcase: [ - 'error', - { - allow: [ 'wc_product_block_data' ], - properties: 'never', - }, - ], 'valid-jsdoc': 'off', }, }; diff --git a/plugins/woocommerce-blocks/assets/js/settings/blocks/constants.js b/plugins/woocommerce-blocks/assets/js/settings/blocks/constants.js new file mode 100644 index 00000000000..01765a680f1 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/blocks/constants.js @@ -0,0 +1,19 @@ +import { getSetting } from '@woocommerce/settings'; + +export const ENABLE_REVIEW_RATING = getSetting( 'enableReviewRating', true ); +export const SHOW_AVATARS = getSetting( 'showAvatars', true ); +export const MAX_COLUMNS = getSetting( 'max_columns', 6 ); +export const MIN_COLUMNS = getSetting( 'min_columns', 1 ); +export const DEFAULT_COLUMNS = getSetting( 'default_columns', 3 ); +export const MAX_ROWS = getSetting( 'max_rows', 6 ); +export const MIN_ROWS = getSetting( 'min_rows', 1 ); +export const DEFAULT_ROWS = getSetting( 'default_rows', 1 ); +export const MIN_HEIGHT = getSetting( 'min_height', 500 ); +export const DEFAULT_HEIGHT = getSetting( 'default_height', 500 ); +export const PLACEHOLDER_IMG_SRC = getSetting( 'placeholderImgSrc ', '' ); +export const THUMBNAIL_SIZE = getSetting( 'thumbnail_size', 300 ); +export const IS_LARGE_CATALOG = getSetting( 'isLargeCatalog' ); +export const LIMIT_TAGS = getSetting( 'limitTags' ); +export const HAS_TAGS = getSetting( 'hasTags', true ); +export const HOME_URL = getSetting( 'homeUrl ', '' ); +export const PRODUCT_CATEGORIES = getSetting( 'productCategories', [] ); diff --git a/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js b/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js index a72feba3964..c6bf3b521e5 100644 --- a/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js +++ b/plugins/woocommerce-blocks/assets/js/settings/blocks/index.js @@ -1,37 +1,2 @@ -const getConstantFromData = ( property, fallback = false ) => { - if ( - typeof wc_product_block_data === 'object' && - wc_product_block_data.hasOwnProperty( property ) - ) { - return wc_product_block_data[ property ]; - } - return fallback; -}; - -export const ENABLE_REVIEW_RATING = getConstantFromData( - 'enableReviewRating', - true -); -export const SHOW_AVATARS = getConstantFromData( 'showAvatars', true ); -export const MAX_COLUMNS = getConstantFromData( 'max_columns', 6 ); -export const MIN_COLUMNS = getConstantFromData( 'min_columns', 1 ); -export const DEFAULT_COLUMNS = getConstantFromData( 'default_columns', 3 ); -export const MAX_ROWS = getConstantFromData( 'max_rows', 6 ); -export const MIN_ROWS = getConstantFromData( 'min_rows', 1 ); -export const DEFAULT_ROWS = getConstantFromData( 'default_rows', 1 ); -export const MIN_HEIGHT = getConstantFromData( 'min_height', 500 ); -export const DEFAULT_HEIGHT = getConstantFromData( 'default_height', 500 ); -export const PLACEHOLDER_IMG_SRC = getConstantFromData( - 'placeholderImgSrc ', - '' -); -export const THUMBNAIL_SIZE = getConstantFromData( 'thumbnail_size', 300 ); -export const IS_LARGE_CATALOG = getConstantFromData( 'isLargeCatalog' ); -export const LIMIT_TAGS = getConstantFromData( 'limitTags' ); -export const HAS_TAGS = getConstantFromData( 'hasTags', true ); -export const HOME_URL = getConstantFromData( 'homeUrl ', '' ); -export const PRODUCT_CATEGORIES = getConstantFromData( - 'productCategories', - [] -); +export * from './constants'; export { ENDPOINTS } from './endpoints'; diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/currency.js b/plugins/woocommerce-blocks/assets/js/settings/shared/currency.js deleted file mode 100644 index 86bc247f707..00000000000 --- a/plugins/woocommerce-blocks/assets/js/settings/shared/currency.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Wrapper for the wcSettings global, which sets defaults if data is missing. - * - * Only settings used by blocks are defined here. Component settings are left out. - */ -const currency = wcSettings.currency || { - code: 'USD', - precision: 2, - symbol: '$', - position: 'left', - decimal_separator: '.', - thousand_separator: ',', - price_format: '%1$s%2$s', -}; - -export default currency; diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.js b/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.js new file mode 100644 index 00000000000..916efe10e0e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/default-constants.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { allSettings } from './settings-init'; + +export const ADMIN_URL = allSettings.adminUrl; +export const COUNTRIES = allSettings.countries; +export const CURRENCY = allSettings.currency; +export const LOCALE = allSettings.locale; +export const ORDER_STATUSES = allSettings.orderStatuses; +export const SITE_TITLE = allSettings.siteTitle; +export const WC_ASSET_URL = allSettings.wcAssetUrl; +export const DEFAULT_DATE_RANGE = allSettings.defaultDateRange; diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/get-setting.js b/plugins/woocommerce-blocks/assets/js/settings/shared/get-setting.js new file mode 100644 index 00000000000..482e69eb043 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/get-setting.js @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { allSettings } from './settings-init'; + +/** + * Retrieves a setting value from the setting state. + * + * @export + * @param {string} name The identifier for the setting. + * @param {mixed} [fallback=false] The value to use as a fallback + * if the setting is not in the + * state. + * @param {function} [filter=( val ) => val] A callback for filtering the + * value before it's returned. + * Receives both the found value + * (if it exists for the key) and + * the provided fallback arg. + * @returns {mixed} + */ +export function getSetting( name, fallback = false, filter = ( val ) => val ) { + const value = allSettings.hasOwnProperty( name ) + ? allSettings[ name ] + : fallback; + return filter( value, fallback ); +} diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/index.js b/plugins/woocommerce-blocks/assets/js/settings/shared/index.js index a2bc812a278..aba43518656 100644 --- a/plugins/woocommerce-blocks/assets/js/settings/shared/index.js +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/index.js @@ -1,2 +1,3 @@ -// Exports shared settings from wcSettings global. -export { default as currency } from './currency'; +export { getSetting } from './get-setting'; +export { setSetting } from './set-setting'; +export * from './default-constants'; diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/set-setting.js b/plugins/woocommerce-blocks/assets/js/settings/shared/set-setting.js new file mode 100644 index 00000000000..d29c02d18b3 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/set-setting.js @@ -0,0 +1,17 @@ +import { allSettings } from './settings-init'; + +/** + * Sets a value to a property on the settings state. + * + * @export + * @param {string} name The setting property key for the + * setting being mutated. + * @param {mixed} value The value to set. + * @param {function} [filter=( val ) => val] Allows for providing a callback + * to sanitize the setting (eg. + * ensure it's a number) + */ +export function setSetting( name, value, filter = ( val ) => val ) { + value = filter( value ); + allSettings[ name ] = filter( value ); +} diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/settings-init.js b/plugins/woocommerce-blocks/assets/js/settings/shared/settings-init.js new file mode 100644 index 00000000000..82a3f26ae4e --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/settings-init.js @@ -0,0 +1,42 @@ +const defaults = { + adminUrl: '', + countries: [], + currency: { + code: 'USD', + precision: 2, + symbol: '$', + symbolPosition: 'left', + decimalSeparator: '.', + priceFormat: '%1$s%2$s', + thousandSeparator: ',', + }, + defaultDateRange: 'period=month&compare=previous_year', + locale: { + siteLocale: 'en_US', + userLocale: 'en_US', + weekdaysShort: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], + }, + orderStatuses: [], + siteTitle: '', + wcAssetUrl: '', +}; + +const globalSharedSettings = typeof wcSettings === 'object' ? wcSettings : {}; + +// Use defaults or global settings, depending on what is set. +const allSettings = { + ...defaults, + ...globalSharedSettings, +}; + +allSettings.currency = { + ...defaults.currency, + ...allSettings.currency, +}; + +allSettings.locale = { + ...defaults.locale, + ...allSettings.locale, +}; + +export { allSettings }; diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/test/get-setting.js b/plugins/woocommerce-blocks/assets/js/settings/shared/test/get-setting.js new file mode 100644 index 00000000000..e20f5a14805 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/test/get-setting.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { getSetting } from '../get-setting'; +import { ADMIN_URL } from '../default-constants'; + +describe( 'getSetting', () => { + it( 'returns provided default for non available setting', () => { + expect( getSetting( 'nada', 'really nada' ) ).toBe( 'really nada' ); + } ); + it( 'returns expected value for existing setting', () => { + expect( getSetting( 'adminUrl', 'not this' ) ).toEqual( ADMIN_URL ); + } ); + it( 'filters value via provided filter callback', () => { + expect( getSetting( 'some value', 'default', () => 42 ) ).toBe( 42 ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/assets/js/settings/shared/test/set-setting.js b/plugins/woocommerce-blocks/assets/js/settings/shared/test/set-setting.js new file mode 100644 index 00000000000..038e0adb722 --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/settings/shared/test/set-setting.js @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import { setSetting } from '../set-setting'; +import { getSetting } from '../get-setting'; + +describe( 'setSetting', () => { + it( 'should add a new value to the settings state for value not present', () => { + setSetting( 'aSetting', 42 ); + expect( getSetting( 'aSetting' ) ).toBe( 42 ); + } ); + it( 'should replace existing value', () => { + setSetting( 'adminUrl', 'not original' ); + expect( getSetting( 'adminUrl' ) ).toBe( 'not original' ); + } ); + it( 'should save the value run through the provided filter', () => { + setSetting( 'aSetting', 'who', () => 42 ); + expect( getSetting( 'aSetting' ) ).toBe( 42 ); + } ); +} ); diff --git a/plugins/woocommerce-blocks/src/Assets.php b/plugins/woocommerce-blocks/src/Assets.php index 1934bfd2f91..b27162e5fcc 100644 --- a/plugins/woocommerce-blocks/src/Assets.php +++ b/plugins/woocommerce-blocks/src/Assets.php @@ -16,19 +16,23 @@ class Assets { /** * Initialize class features on init. + * + * @since $VID:$ + * Moved most initialization to BootStrap and AssetDataRegistry + * classes as a part of ongoing refactor */ public static function init() { add_action( 'init', array( __CLASS__, 'register_assets' ) ); - add_action( 'admin_print_scripts', array( __CLASS__, 'print_shared_settings' ), 1 ); - add_action( 'admin_print_scripts', array( __CLASS__, 'maybe_add_asset_data' ), 1 ); - add_action( 'admin_print_footer_scripts', array( __CLASS__, 'maybe_add_asset_data' ), 1 ); - add_action( 'wp_print_scripts', array( __CLASS__, 'maybe_add_asset_data' ), 1 ); - add_action( 'wp_print_footer_scripts', array( __CLASS__, 'maybe_add_asset_data' ), 1 ); add_action( 'body_class', array( __CLASS__, 'add_theme_body_class' ), 1 ); + add_filter( 'woocommerce_shared_settings', array( __CLASS__, 'get_wc_block_data' ) ); } /** * Register block scripts & styles. + * + * @since $VID:$ + * Moved data related enqueuing to new AssetDataRegistry class + * as part of ongoing refactoring. */ public static function register_assets() { self::register_style( 'wc-block-editor', plugins_url( 'build/editor.css', __DIR__ ), array( 'wp-edit-blocks' ) ); @@ -37,10 +41,8 @@ class Assets { wp_style_add_data( 'wc-block-style', 'rtl', 'replace' ); // Shared libraries and components across all blocks. - self::register_script( 'wc-shared-settings', plugins_url( 'build/wc-shared-settings.js', __DIR__ ), [], false ); - self::register_script( 'wc-block-settings', plugins_url( 'build/wc-block-settings.js', __DIR__ ), [], false ); self::register_script( 'wc-blocks', plugins_url( 'build/blocks.js', __DIR__ ), [], false ); - self::register_script( 'wc-vendors', plugins_url( 'build/vendors.js', __DIR__ ), [ 'wc-shared-settings' ], false ); + self::register_script( 'wc-vendors', plugins_url( 'build/vendors.js', __DIR__ ), [], false ); // Individual blocks. self::register_script( 'wc-handpicked-products', plugins_url( 'build/handpicked-products.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); @@ -60,30 +62,6 @@ class Assets { self::register_script( 'wc-product-search', plugins_url( 'build/product-search.js', __DIR__ ), array( 'wc-vendors', 'wc-blocks' ) ); } - /** - * Print wcSettings in all pages. This is a temporary fix until we find a better - * solution to share settings between WooCommerce Admin and WooCommerce Blocks. - * See https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/932 - */ - public static function print_shared_settings() { - echo ''; - } - - /** - * Attach data to registered assets using inline scripts. - */ - public static function maybe_add_asset_data() { - if ( wp_script_is( 'wc-block-settings', 'enqueued' ) ) { - wp_add_inline_script( - 'wc-block-settings', - self::get_wc_block_data(), - 'before' - ); - } - } - /** * Add body classes. * @@ -95,50 +73,17 @@ class Assets { return $classes; } - /** - * Returns javascript to inject as data for enqueued wc-shared-settings script. - * - * @return string; - * @since 2.4.0 - */ - protected static function get_wc_settings_data() { - global $wp_locale; - $code = get_woocommerce_currency(); - $settings = apply_filters( - 'woocommerce_components_settings', - array( - 'adminUrl' => admin_url(), - 'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ), - 'siteLocale' => esc_attr( get_bloginfo( 'language' ) ), - 'currency' => array( - 'code' => $code, - 'precision' => wc_get_price_decimals(), - 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $code ) ), - 'position' => get_option( 'woocommerce_currency_pos' ), - 'decimal_separator' => wc_get_price_decimal_separator(), - 'thousand_separator' => wc_get_price_thousand_separator(), - 'price_format' => html_entity_decode( get_woocommerce_price_format() ), - ), - 'stockStatuses' => wc_get_product_stock_status_options(), - 'siteTitle' => get_bloginfo( 'name' ), - 'dataEndpoints' => [], - 'l10n' => array( - 'userLocale' => get_user_locale(), - 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), - ), - ) - ); - return rawurlencode( wp_json_encode( $settings ) ); - } - /** * Returns block-related data for enqueued wc-block-settings script. * * This is used to map site settings & data into JS-accessible variables. * + * @param array $settings The original settings array from the filter. + * * @since 2.4.0 + * @since $VID:$ returned merged data along with incoming $settings */ - protected static function get_wc_block_data() { + public static function get_wc_block_data( $settings ) { $tag_count = wp_count_terms( 'product_tag' ); $product_counts = wp_count_posts( 'product' ); $product_categories = get_terms( @@ -153,27 +98,28 @@ class Assets { } // Global settings used in each block. - $block_settings = array( - 'min_columns' => wc_get_theme_support( 'product_blocks::min_columns', 1 ), - 'max_columns' => wc_get_theme_support( 'product_blocks::max_columns', 6 ), - 'default_columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ), - 'min_rows' => wc_get_theme_support( 'product_blocks::min_rows', 1 ), - 'max_rows' => wc_get_theme_support( 'product_blocks::max_rows', 6 ), - 'default_rows' => wc_get_theme_support( 'product_blocks::default_rows', 1 ), - 'thumbnail_size' => wc_get_theme_support( 'thumbnail_image_width', 300 ), - 'placeholderImgSrc' => wc_placeholder_img_src(), - 'min_height' => wc_get_theme_support( 'featured_block::min_height', 500 ), - 'default_height' => wc_get_theme_support( 'featured_block::default_height', 500 ), - 'isLargeCatalog' => $product_counts->publish > 200, - 'limitTags' => $tag_count > 100, - 'hasTags' => $tag_count > 0, - 'productCategories' => $product_categories, - 'homeUrl' => esc_js( home_url( '/' ) ), - 'showAvatars' => '1' === get_option( 'show_avatars' ), - 'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ), + return array_merge( + $settings, + [ + 'min_columns' => wc_get_theme_support( 'product_blocks::min_columns', 1 ), + 'max_columns' => wc_get_theme_support( 'product_blocks::max_columns', 6 ), + 'default_columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ), + 'min_rows' => wc_get_theme_support( 'product_blocks::min_rows', 1 ), + 'max_rows' => wc_get_theme_support( 'product_blocks::max_rows', 6 ), + 'default_rows' => wc_get_theme_support( 'product_blocks::default_rows', 1 ), + 'thumbnail_size' => wc_get_theme_support( 'thumbnail_image_width', 300 ), + 'placeholderImgSrc' => wc_placeholder_img_src(), + 'min_height' => wc_get_theme_support( 'featured_block::min_height', 500 ), + 'default_height' => wc_get_theme_support( 'featured_block::default_height', 500 ), + 'isLargeCatalog' => $product_counts->publish > 200, + 'limitTags' => $tag_count > 100, + 'hasTags' => $tag_count > 0, + 'productCategories' => $product_categories, + 'homeUrl' => esc_url( home_url( '/' ) ), + 'showAvatars' => '1' === get_option( 'show_avatars' ), + 'enableReviewRating' => 'yes' === get_option( 'woocommerce_enable_review_rating' ), + ] ); - $block_settings = rawurlencode( wp_json_encode( $block_settings ) ); - return "var wc_product_block_data = JSON.parse( decodeURIComponent( '" . $block_settings . "' ) );"; } /** diff --git a/plugins/woocommerce-blocks/src/Assets/Api.php b/plugins/woocommerce-blocks/src/Assets/Api.php new file mode 100644 index 00000000000..22ac200a936 --- /dev/null +++ b/plugins/woocommerce-blocks/src/Assets/Api.php @@ -0,0 +1,150 @@ +package = $package; + } + + /** + * Get the file modified time as a cache buster if we're in dev mode. + * + * @param string $file Local path to the file (relative to the plugin + * directory). + * @return string The cache buster value to use for the given file. + */ + protected function get_file_version( $file ) { + if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { + return filemtime( $this->package->get_path( trim( $file, '/' ) ) ); + } + return $this->package->get_version(); + } + + /** + * Retrieve the url to an asset for this plugin. + * + * @param string $relative_path An optional relative path appended to the + * returned url. + * + * @return string + */ + protected function get_asset_url( $relative_path = '' ) { + return $this->package->get_url( $relative_path ); + } + + /** + * Returns the dependency array for the given asset relative path. + * + * @param string $asset_relative_path Something like 'build/constants.js' + * considered to be relative to the main + * asset path. + * @param array $extra_dependencies Extra dependencies to be explicitly + * added to the generated array. + * + * @return array An array of dependencies + */ + protected function get_dependencies( + $asset_relative_path, + $extra_dependencies = [] + ) { + $dependency_path = $this->package->get_path( + str_replace( '.js', '.deps.json', $asset_relative_path ) + ); + // phpcs:ignore WordPress.WP.AlternativeFunctions + $dependencies = file_exists( $dependency_path ) + // phpcs:ignore WordPress.WP.AlternativeFunctions + ? json_decode( file_get_contents( $dependency_path ) ) + : []; + return array_merge( $dependencies, $extra_dependencies ); + } + + /** + * Registers a script according to `wp_register_script`, additionally + * loading the translations for the file. + * + * @since $VID:$ + * + * @param string $handle Name of the script. Should be unique. + * @param string $relative_src Relative url for the script to the path + * from plugin root. + * @param array $deps Optional. An array of registered script + * handles this script depends on. Default + * empty array. + * @param bool $has_i18n Optional. Whether to add a script + * translation call to this file. Default: + * true. + */ + public function register_script( $handle, $relative_src, $deps = [], $has_i18n = true ) { + wp_register_script( + $handle, + $this->get_asset_url( $relative_src ), + $this->get_dependencies( $relative_src, $deps ), + $this->get_file_version( $relative_src ), + true + ); + if ( $has_i18n && function_exists( 'wp_set_script_translations' ) ) { + wp_set_script_translations( + $handle, + 'woo-gutenberg-products-block', + $this->package->get_path( 'languages' ) + ); + } + } + + /** + * Queues a block script. + * + * @since $VID:$ + * + * @param string $name Name of the script used to identify the file inside build folder. + */ + public function register_block_script( $name ) { + $src = 'build/' . $name . '.js'; + $handle = 'wc-' . $name; + $this->register_script( $handle, $src ); + wp_enqueue_script( $handle ); + } + + /** + * Registers a style according to `wp_register_style`. + * + * @since $VID:$ + * + * @param string $handle Name of the stylesheet. Should be unique. + * @param string $src Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. + * @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array. + * @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like + * 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. + */ + public function register_style( $handle, $src, $deps = [], $media = 'all' ) { + $filename = str_replace( plugins_url( '/', __DIR__ ), '', $src ); + $ver = $this->get_file_version( $filename ); + wp_register_style( $handle, $src, $deps, $ver, $media ); + } +} diff --git a/plugins/woocommerce-blocks/src/Assets/AssetDataRegistry.php b/plugins/woocommerce-blocks/src/Assets/AssetDataRegistry.php new file mode 100644 index 00000000000..c4736a9387d --- /dev/null +++ b/plugins/woocommerce-blocks/src/Assets/AssetDataRegistry.php @@ -0,0 +1,268 @@ +api = $asset_api; + $this->init(); + } + + /** + * Hook into WP asset registration for enqueueing asset data. + */ + protected function init() { + add_action( 'init', array( $this, 'register_data_script' ) ); + add_action( 'wp_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 1 ); + add_action( 'admin_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 1 ); + } + + /** + * Exposes core asset data + * + * @return array An array containing core data. + */ + protected function get_core_data() { + global $wp_locale; + $currency = get_woocommerce_currency(); + return [ + 'adminUrl' => admin_url(), + 'countries' => WC()->countries->get_countries(), + 'currency' => [ + 'code' => $currency, + 'precision' => wc_get_price_decimals(), + 'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $currency ) ), + 'symbolPosition' => get_option( 'woocommerce_currency_pos' ), + 'decimalSeparator' => wc_get_price_decimal_separator(), + 'thousandSeparator' => wc_get_price_thousand_separator(), + 'priceFormat' => html_entity_decode( get_woocommerce_price_format() ), + ], + 'locale' => [ + 'siteLocale' => get_locale(), + 'userLocale' => get_user_locale(), + 'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ), + ], + 'orderStatuses' => $this->get_order_statuses( wc_get_order_statuses() ), + 'siteTitle' => get_bloginfo( 'name ' ), + 'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ), + ]; + } + + /** + * Returns block-related data for enqueued wc-settings script. + * Format order statuses by removing a leading 'wc-' if present. + * + * @param array $statuses Order statuses. + * @return array formatted statuses. + */ + protected function get_order_statuses( $statuses ) { + $formatted_statuses = array(); + foreach ( $statuses as $key => $value ) { + $formatted_key = preg_replace( '/^wc-/', '', $key ); + $formatted_statuses[ $formatted_key ] = $value; + } + return $formatted_statuses; + } + + /** + * Used for on demand initialization of asset data and registering it with + * the internal data registry. + * + * Note: core data will overwrite any externally registered data via the api. + */ + protected function initialize_core_data() { + /** + * Low level hook for registration of new data late in the cycle. + * + * Developers, do not use this hook as it is likely to be removed. + * Instead, use the data api: + * wc_blocks_container() + * ->get( Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry::class ) + * ->add( $key, $value ) + */ + $settings = apply_filters( + 'woocommerce_shared_settings', + $this->data + ); + // note this WILL wipe any data already registered to these keys because + // they are protected. + $this->data = array_merge_recursive( $settings, $this->get_core_data() ); + } + + /** + * Loops through each registered lazy data callback and adds the returned + * value to the data array. + * + * This method is executed right before preparing the data for printing to + * the rendered screen. + * + * @return void + */ + protected function execute_lazy_data() { + foreach ( $this->lazy_data as $key => $callback ) { + $this->data[ $key ] = $callback(); + } + } + + /** + * Exposes private registered data to child classes. + * + * @return array The registered data on the private data property + */ + protected function get() { + return $this->data; + } + + /** + * Interface for adding data to the registry. + * + * @param string $key The key used to reference the data being registered. + * You can only register data that is not already in the + * registry identified by the given key. + * @param mixed $data If not a function, registered to the registry as is. + * If a function, then the callback is invoked right + * before output to the screen. + * + * @throws InvalidArgumentException Only throws when site is in debug mode. + * Always logs the error. + */ + public function add( $key, $data ) { + try { + $this->add_data( $key, $data ); + } catch ( Exception $e ) { + if ( $this->debug() ) { + // bubble up. + throw $e; + } + wc_caught_exception( $e, __METHOD__, [ $key, $data ] ); + } + } + + /** + * Callback for registering the data script via WordPress API. + * + * @return void + */ + public function register_data_script() { + $this->api->register_script( + $this->handle, + 'build/wc-settings.js', + [], + false + ); + } + + /** + * Callback for enqueuing asset data via the WP api. + * + * Note: while this is hooked into print/admin_print_scripts, it still only + * happens if the script attached to `wc-settings` handle is enqueued. This + * is done to allow for any potentially expensive data generation to only + * happen for routes that need it. + */ + public function enqueue_asset_data() { + if ( wp_script_is( $this->handle, 'enqueued' ) ) { + $this->initialize_core_data(); + $this->execute_lazy_data(); + $data = rawurlencode( wp_json_encode( $this->data ) ); + wp_add_inline_script( + $this->handle, + "var wcSettings = wcSettings || JSON.parse( decodeURIComponent( '" + . esc_js( $data ) + . "' ) );", + 'before' + ); + } + } + + /** + * See self::add() for docs. + * + * @param string $key Key for the data. + * @param mixed $data Value for the data. + * + * @throws InvalidArgumentException If key is not a string or already + * exists in internal data cache. + */ + protected function add_data( $key, $data ) { + if ( ! is_string( $key ) ) { + if ( $this->debug() ) { + throw new InvalidArgumentException( + 'Key for the data being registered must be a string' + ); + } + } + if ( isset( $this->data[ $key ] ) ) { + if ( $this->debug() ) { + throw new InvalidArgumentException( + 'Overriding existing data with an already registered key is not allowed' + ); + } + return; + } + if ( \method_exists( $data, '__invoke' ) ) { + $this->lazy_data[ $key ] = $data; + return; + } + $this->data[ $key ] = $data; + } + + /** + * Exposes whether the current site is in debug mode or not. + * + * @return boolean True means the site is in debug mode. + */ + protected function debug() { + return defined( 'WP_DEBUG' ) && WP_DEBUG; + } +} diff --git a/plugins/woocommerce-blocks/src/Assets/BackCompatAssetDataRegistry.php b/plugins/woocommerce-blocks/src/Assets/BackCompatAssetDataRegistry.php new file mode 100644 index 00000000000..bc3ee48501f --- /dev/null +++ b/plugins/woocommerce-blocks/src/Assets/BackCompatAssetDataRegistry.php @@ -0,0 +1,79 @@ +initialize_core_data(); + $this->execute_lazy_data(); + /** + * Back-compat filter, developers, use 'woocommerce_shared_settings' + * filter, not this one. + * + * @deprecated $VID:$ + */ + $data = apply_filters( + 'woocommerce_components_settings', + $this->get() + ); + + $data = rawurlencode( wp_json_encode( $data ) ); + // for back compat with wc-admin (or other plugins) that expects + // wcSettings to be always available. + // @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/932. + echo ''; + } + + /** + * Override parent method. + * + * @see AssetDataRegistry::get_core_data + * + * @return array An array of data to output for enqueued script. + */ + protected function get_core_data() { + global $wp_locale; + $core_data = parent::get_core_data(); + return array_merge( + $core_data, + [ + 'siteLocale' => $core_data['locale']['siteLocale'], + 'stockStatuses' => $core_data['orderStatuses'], + 'dataEndpoints' => [], + 'l10n' => [ + 'userLocale' => $core_data['locale']['userLocale'], + 'weekdaysShort' => $core_data['locale']['weekdaysShort'], + ], + 'currency' => array_merge( + $core_data['currency'], + [ + 'position' => $core_data['currency']['symbolPosition'], + 'decimal_separator' => $core_data['currency']['decimalSeparator'], + 'thousand_separator' => $core_data['currency']['thousandSeparator'], + 'price_format' => $core_data['currency']['priceFormat'], + ] + ), + ] + ); + } +} diff --git a/plugins/woocommerce-blocks/src/Domain/Bootstrap.php b/plugins/woocommerce-blocks/src/Domain/Bootstrap.php new file mode 100644 index 00000000000..e3dc410bdf2 --- /dev/null +++ b/plugins/woocommerce-blocks/src/Domain/Bootstrap.php @@ -0,0 +1,151 @@ +container = $container; + $this->package = $container->get( Package::class ); + $this->init(); + /** + * Usable as a safe event hook for when the plugin has been loaded. + */ + do_action( 'woocommerce_blocks_loaded' ); + } + + /** + * Init the package - load the blocks library and define constants. + */ + public function init() { + if ( ! $this->has_dependencies() ) { + return; + } + + $this->remove_core_blocks(); + + if ( ! $this->is_built() ) { + $this->add_build_notice(); + } + + // register core dependencies with the container. + $this->container->register( + AssetApi::class, + function ( Container $container ) { + return new AssetApi( $container->get( Package::class ) ); + } + ); + $this->container->register( + AssetDataRegistry::class, + function( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + $load_back_compat = ( + defined( 'WC_ADMIN_VERSION_NUMBER' ) + && version_compare( WC_ADMIN_VERSION_NUMBER, '0.19.0', '<=' ) + ) || + ( + version_compare( WC_VERSION, '3.7.0', '<=' ) + ); + return $load_back_compat + ? new BackCompatAssetDataRegistry( $asset_api ) + : new AssetDataRegistry( $asset_api ); + } + ); + + // load AssetDataRegistry. + $this->container->get( AssetDataRegistry::class ); + + Library::init(); + OldAssets::init(); + RestApi::init(); + } + + /** + * Check dependencies exist. + * + * @return boolean + */ + protected function has_dependencies() { + return class_exists( 'WooCommerce' ) && function_exists( 'register_block_type' ); + } + + /** + * See if files have been built or not. + * + * @return bool + */ + protected function is_built() { + return file_exists( + $this->package->get_path( 'build/featured-product.js' ) + ); + } + + /** + * Add a notice stating that the build has not been done yet. + */ + protected function add_build_notice() { + add_action( + 'admin_notices', + function() { + echo '

'; + printf( + /* Translators: %1$s is the install command, %2$s is the build command, %3$s is the watch command. */ + esc_html__( 'WooCommerce Blocks development mode requires files to be built. From the plugin directory, run %1$s to install dependencies, %2$s to build the files or %3$s to build the files and watch for changes.', 'woo-gutenberg-products-block' ), + 'npm install', + 'npm run build', + 'npm start' + ); + echo '

'; + } + ); + } + + /** + * Remove core blocks (for 3.6 and above). + */ + protected function remove_core_blocks() { + remove_action( 'init', array( 'WC_Block_Library', 'init' ) ); + remove_action( 'init', array( 'WC_Block_Library', 'register_blocks' ) ); + remove_action( 'init', array( 'WC_Block_Library', 'register_assets' ) ); + remove_filter( 'block_categories', array( 'WC_Block_Library', 'add_block_category' ) ); + remove_action( 'admin_print_footer_scripts', array( 'WC_Block_Library', 'print_script_settings' ), 1 ); + remove_action( 'init', array( 'WGPB_Block_Library', 'init' ) ); + } +} diff --git a/plugins/woocommerce-blocks/src/Domain/Package.php b/plugins/woocommerce-blocks/src/Domain/Package.php new file mode 100644 index 00000000000..b9436dc251c --- /dev/null +++ b/plugins/woocommerce-blocks/src/Domain/Package.php @@ -0,0 +1,95 @@ +version = $version; + $this->plugin_file = $plugin_file; + $this->path = dirname( $plugin_file ); + } + + /** + * Returns the version of the plugin. + * + * @return string + */ + public function get_version() { + return $this->version; + } + + /** + * Returns the path to the main plugin file. + * + * @return string + */ + public function get_plugin_file() { + return $this->plugin_file; + } + + /** + * Returns the path to the plugin directory. + * + * @param string $relative_path If provided, the relative path will be + * appended to the plugin path. + * + * @return string + */ + public function get_path( $relative_path = '' ) { + return '' === $relative_path + ? trailingslashit( $this->path ) + : trailingslashit( $this->path ) . $relative_path; + } + + /** + * Returns the url to the plugin directory. + * + * @param string $relative_url If provided, the relative url will be + * appended to the plugin url. + * + * @return string + */ + public function get_url( $relative_url = '' ) { + return '' === $relative_url + ? plugin_dir_url( $this->get_plugin_file() ) + : plugin_dir_url( $this->get_plugin_file() ) . $relative_url; + } +} diff --git a/plugins/woocommerce-blocks/src/Package.php b/plugins/woocommerce-blocks/src/Package.php index 69a8f2dec93..1b132848c32 100644 --- a/plugins/woocommerce-blocks/src/Package.php +++ b/plugins/woocommerce-blocks/src/Package.php @@ -7,45 +7,36 @@ namespace Automattic\WooCommerce\Blocks; +use Automattic\WooCommerce\Blocks\Domain\Package as NewPackage; + defined( 'ABSPATH' ) || exit; /** * Main package class. + * + * @deprecated $VID:$ */ class Package { /** - * Version. + * For back compat this is provided. Ideally, you should register your + * class with Automattic\Woocommerce\Blocks\Container and make Package a + * dependency. * - * @var string + * @since $VID:$ + * @return Package The Package instance class */ - const VERSION = '2.5.0-dev'; - - /** - * Stores if init has ran yet. - * - * @var boolean - */ - protected static $did_init = false; + protected static function get_package() { + return wc_blocks_container()->get( NewPackage::class ); + } /** * Init the package - load the blocks library and define constants. + * + * @since $VID:$ Handled by new NewPackage. */ public static function init() { - if ( true === self::$did_init || ! self::has_dependencies() ) { - return; - } - - self::$did_init = true; - self::remove_core_blocks(); - - if ( ! self::is_built() ) { - self::add_build_notice(); - } - - Library::init(); - Assets::init(); - RestApi::init(); + // noop. } /** @@ -54,7 +45,7 @@ class Package { * @return string */ public static function get_version() { - return self::VERSION; + return self::get_package()->get_version(); } /** @@ -63,57 +54,6 @@ class Package { * @return string */ public static function get_path() { - return dirname( __DIR__ ); + return self::get_package()->get_path(); } - - /** - * Check dependencies exist. - * - * @return boolean - */ - protected static function has_dependencies() { - return class_exists( 'WooCommerce' ) && function_exists( 'register_block_type' ); - } - - /** - * See if files have been built or not. - * - * @return bool - */ - protected static function is_built() { - return file_exists( dirname( __DIR__ ) . '/build/featured-product.js' ); - } - - /** - * Add a notice stating that the build has not been done yet. - */ - protected static function add_build_notice() { - add_action( - 'admin_notices', - function() { - echo '

'; - printf( - /* Translators: %1$s is the install command, %2$s is the build command, %3$s is the watch command. */ - esc_html__( 'WooCommerce Blocks development mode requires files to be built. From the plugin directory, run %1$s to install dependencies, %2$s to build the files or %3$s to build the files and watch for changes.', 'woo-gutenberg-products-block' ), - 'npm install', - 'npm run build', - 'npm start' - ); - echo '

'; - } - ); - } - - /** - * Remove core blocks (for 3.6 and below). - */ - protected static function remove_core_blocks() { - remove_action( 'init', array( 'WC_Block_Library', 'init' ) ); - remove_action( 'init', array( 'WC_Block_Library', 'register_blocks' ) ); - remove_action( 'init', array( 'WC_Block_Library', 'register_assets' ) ); - remove_filter( 'block_categories', array( 'WC_Block_Library', 'add_block_category' ) ); - remove_action( 'admin_print_footer_scripts', array( 'WC_Block_Library', 'print_script_settings' ), 1 ); - remove_action( 'init', array( 'WGPB_Block_Library', 'init' ) ); - } - } diff --git a/plugins/woocommerce-blocks/src/Registry/AbstractDependencyType.php b/plugins/woocommerce-blocks/src/Registry/AbstractDependencyType.php new file mode 100644 index 00000000000..65444074cef --- /dev/null +++ b/plugins/woocommerce-blocks/src/Registry/AbstractDependencyType.php @@ -0,0 +1,60 @@ +callable_or_value = $callable_or_value; + } + + /** + * Resolver for the internal dependency value. + * + * @param Container $container The Dependency Injection Container. + * + * @return mixed + */ + protected function resolve_value( Container $container ) { + $callback = $this->callable_or_value; + return \method_exists( $callback, '__invoke' ) + ? $callback( $container ) + : $callback; + } + + /** + * Retrieves the value stored internally for this DependencyType + * + * @param Container $container The Dependency Injection Container. + * + * @return void + */ + abstract public function get( Container $container ); +} diff --git a/plugins/woocommerce-blocks/src/Registry/Container.php b/plugins/woocommerce-blocks/src/Registry/Container.php new file mode 100644 index 00000000000..b27c5d4c3ef --- /dev/null +++ b/plugins/woocommerce-blocks/src/Registry/Container.php @@ -0,0 +1,104 @@ +register( MyClass::class, $container->factory( $mycallback ) ); + * ``` + * + * @param Closure $instantiation_callback This will be invoked when the + * dependency is required. It will + * receive an instance of this + * container so the callback can + * retrieve dependencies from the + * container. + * + * @return FactoryType An instance of the FactoryType dependency. + */ + public function factory( Closure $instantiation_callback ) { + return new FactoryType( $instantiation_callback ); + } + + /** + * Interface for registering a new dependency with the container. + * + * By default, the $value will be added as a shared dependency. This means + * that it will be a single instance shared among any other classes having + * that dependency. + * + * If you want a new instance everytime it's required, then wrap the value + * in a call to the factory method (@see Container::factory for example) + * + * Note: Currently if the provided id already is registered in the container, + * the provided value is ignored. + * + * @param string $id A unique string identifier for the provided value. + * Typically it's the fully qualified name for the + * dependency. + * @param mixed $value The value for the dependency. Typically, this is a + * closure that will create the class instance needed. + */ + public function register( $id, $value ) { + if ( empty( $this->registry[ $id ] ) ) { + if ( ! $value instanceof FactoryType ) { + $value = new SharedType( $value ); + } + $this->registry[ $id ] = $value; + } + } + + /** + * Interface for retrieving the dependency stored in the container for the + * given identifier. + * + * @param string $id The identifier for the dependency being retrieved. + * @throws Exception If there is no dependency for the given identifier in + * the container. + * + * @return mixed Typically a class instance. + */ + public function get( $id ) { + if ( ! isset( $this->registry[ $id ] ) ) { + // this is a developer facing exception, hence it is not localized. + throw new Exception( + sprintf( + 'Cannot construct an instance of %s because it has not been registered.', + $id + ) + ); + } + return $this->registry[ $id ]->get( $this ); + } +} diff --git a/plugins/woocommerce-blocks/src/Registry/FactoryType.php b/plugins/woocommerce-blocks/src/Registry/FactoryType.php new file mode 100644 index 00000000000..fb4b6fcde6d --- /dev/null +++ b/plugins/woocommerce-blocks/src/Registry/FactoryType.php @@ -0,0 +1,27 @@ +resolve_value( $container ); + } +} diff --git a/plugins/woocommerce-blocks/src/Registry/SharedType.php b/plugins/woocommerce-blocks/src/Registry/SharedType.php new file mode 100644 index 00000000000..74b711d3b20 --- /dev/null +++ b/plugins/woocommerce-blocks/src/Registry/SharedType.php @@ -0,0 +1,38 @@ +shared_instance ) ) { + $this->shared_instance = $this->resolve_value( $container ); + } + return $this->shared_instance; + } +} diff --git a/plugins/woocommerce-blocks/tests/js/setup-globals.js b/plugins/woocommerce-blocks/tests/js/setup-globals.js index d03202c3512..55d03281adc 100644 --- a/plugins/woocommerce-blocks/tests/js/setup-globals.js +++ b/plugins/woocommerce-blocks/tests/js/setup-globals.js @@ -4,8 +4,12 @@ global.wp = {}; // wcSettings is required by @woocommerce/* packages global.wcSettings = { adminUrl: 'https://vagrant.local/wp/wp-admin/', - locale: 'en-US', - currency: { code: 'USD', precision: 2, symbol: '$' }, + countries: [], + currency: { + code: 'USD', + precision: 2, + symbol: '$', + }, date: { dow: 0, }, @@ -18,7 +22,8 @@ global.wcSettings = { refunded: 'Refunded', failed: 'Failed', }, - l10n: { + locale: { + siteLocale: 'en_US', userLocale: 'en_US', weekdaysShort: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ], }, diff --git a/plugins/woocommerce-blocks/tests/php/Assets/AssetDataRegistry.php b/plugins/woocommerce-blocks/tests/php/Assets/AssetDataRegistry.php new file mode 100644 index 00000000000..47f1ecb331f --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/Assets/AssetDataRegistry.php @@ -0,0 +1,55 @@ +registry = new AssetDataRegistryMock( + wc_blocks_container()->get( API::class ) + ); + } + + public function test_initial_data() { + $this->assertEmpty( $this->registry->get() ); + } + + public function test_add_data() { + $this->registry->add( 'test', 'foo' ); + $this->assertEquals( [ 'test' => 'foo' ], $this->registry->get() ); + } + + public function test_add_lazy_data() { + $lazy = function () { + return 'bar'; + }; + $this->registry->add( 'foo', $lazy ); + // should not be in data yet + $this->assertEmpty( $this->registry->get() ); + $this->registry->execute_lazy_data(); + // should be in data now + $this->assertEquals( [ 'foo' => 'bar' ], $this->registry->get() ); + } + + public function test_invalid_key_on_adding_data() { + $this->expectException( InvalidArgumentException::class ); + $this->registry->add( [ 'some_value' ], 'foo' ); + } + + public function test_already_existing_key_on_adding_data() { + $this->registry->add( 'foo', 'bar' ); + $this->expectException( InvalidArgumentException::class ); + $this->registry->add( 'foo', 'yar' ); + } +} diff --git a/plugins/woocommerce-blocks/tests/php/Bootstrap/MainFile.php b/plugins/woocommerce-blocks/tests/php/Bootstrap/MainFile.php new file mode 100644 index 00000000000..9bd67417ddc --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/Bootstrap/MainFile.php @@ -0,0 +1,49 @@ +container = wc_blocks_container( true ); + } + + public function test_wc_blocks_container_returns_same_instance() { + $container = wc_blocks_container(); + $this->assertSame( $container, $this->container ); + } + + public function test_wc_blocks_container_reset() { + $container = wc_blocks_container( true ); + $this->assertNotSame( $container, $this->container ); + } + + public function wc_blocks_bootstrap() { + $this->assertInstanceOf( Bootstrap::class, wc_blocks_bootstrap() ); + } +} diff --git a/plugins/woocommerce-blocks/tests/php/Domain/Package.php b/plugins/woocommerce-blocks/tests/php/Domain/Package.php new file mode 100644 index 00000000000..74e670263b2 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/Domain/Package.php @@ -0,0 +1,49 @@ +assertEquals( '1.0.0', $this->get_package()->get_version() ); + } + + public function test_get_plugin_file() { + $this->assertEquals( __FILE__, $this->get_package()->get_plugin_file() ); + } + + public function test_get_path() { + $package = $this->get_package(); + // test without relative + $this->assertEquals( dirname( __FILE__ ) . '/', $package->get_path() ); + + //test with relative + $expect = dirname( __FILE__ ) . '/build/test'; + $this->assertEquals( $expect, $package->get_path( 'build/test') ); + } + + public function test_get_url() { + $package = $this->get_package(); + $test_url = plugin_dir_url( __FILE__ ); + // test without relative + $this->assertEquals( $test_url, $package->get_url() ); + + //test with relative + $this->assertEquals( + $test_url . 'build/test', + $package->get_url( 'build/test' ) + ); + } +} diff --git a/plugins/woocommerce-blocks/tests/php/Registry/Container.php b/plugins/woocommerce-blocks/tests/php/Registry/Container.php new file mode 100644 index 00000000000..4997fdecb42 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/Registry/Container.php @@ -0,0 +1,78 @@ +container = new ContainerTest; + } + + public function test_factory() { + $factory = $this->container->factory( function () { return 'foo'; } ); + $this->assertInstanceOf( FactoryType::class, $factory ); + } + + public function test_registering_factory_type() { + $this->container->register( + MockTestDependency::class, + $this->container->factory( + function () { return new MockTestDependency; } + ) + ); + $instanceA = $this->container->get( MockTestDependency::class ); + $instanceB = $this->container->get( MockTestDependency::class ); + + // should not be the same instance; + $this->assertNotSame( $instanceA, $instanceB ); + } + + public function test_registering_shared_type() { + $this->container->register( + MockTestDependency::class, + function () { return new MockTestDependency; } + ); + $instanceA = $this->container->get( MockTestDependency::class ); + $instanceB = $this->container->get( MockTestDependency::class ); + + // should not be the same instance; + $this->assertSame( $instanceA, $instanceB ); + } + + public function test_registering_shared_type_dependent_on_another_shared_type() { + $this->container->register( + MockTestDependency::class . 'A', + function() { return new MockTestDependency; } + ); + $this->container->register( + MockTestDependency::class . 'B', + function( $container ) { + return new MockTestDependency( + $container->get( MockTestDependency::class . 'A' ) + ); + } + ); + $instanceA = $this->container->get( MockTestDependency::class . 'A' ); + $instanceB = $this->container->get( MockTestDependency::class . 'B' ); + + // should not be the same instance + $this->assertNotSame( $instanceA, $instanceB ); + + // dependency on B should be the same as A + $this->assertSame( $instanceA, $instanceB->dependency ); + } +} diff --git a/plugins/woocommerce-blocks/tests/php/mocks/AssetDataRegistry.php b/plugins/woocommerce-blocks/tests/php/mocks/AssetDataRegistry.php new file mode 100644 index 00000000000..4898adfeb8a --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/mocks/AssetDataRegistry.php @@ -0,0 +1,26 @@ +debug = $debug; + } + + protected function debug() { + return $this->debug; + } +} diff --git a/plugins/woocommerce-blocks/tests/php/mocks/MockTestDependency.php b/plugins/woocommerce-blocks/tests/php/mocks/MockTestDependency.php new file mode 100644 index 00000000000..7359a2d0430 --- /dev/null +++ b/plugins/woocommerce-blocks/tests/php/mocks/MockTestDependency.php @@ -0,0 +1,11 @@ +dependency = $dependency; + } +}; diff --git a/plugins/woocommerce-blocks/webpack.config.js b/plugins/woocommerce-blocks/webpack.config.js index da0b9b26db9..a04e523f53d 100644 --- a/plugins/woocommerce-blocks/webpack.config.js +++ b/plugins/woocommerce-blocks/webpack.config.js @@ -2,6 +2,7 @@ * External dependencies */ const path = require( 'path' ); +const { kebabCase } = require( 'lodash' ); const MergeExtractFilesPlugin = require( './bin/merge-extract-files-webpack-plugin' ); const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); @@ -36,10 +37,16 @@ const baseConfig = { }, }; +const alias = { + '@woocommerce/block-settings': path.resolve( + __dirname, + 'assets/js/settings/blocks' + ), +}; + const requestToExternal = ( request ) => { const wcDepMap = { - '@woocommerce/settings': [ 'wc', 'wc-shared-settings' ], - '@woocommerce/block-settings': [ 'wc', 'wc-block-settings' ], + '@woocommerce/settings': { this: [ 'wc', 'wcSettings' ] }, }; if ( wcDepMap[ request ] ) { return wcDepMap[ request ]; @@ -48,8 +55,8 @@ const requestToExternal = ( request ) => { const requestToHandle = ( request ) => { const wcHandleMap = { - '@woocommerce/settings': 'wc-shared-settings', - '@woocommerce/block-settings': 'wc-block-settings', + '@woocommerce/settings': 'wc-settings', + '@woocommerce/block-settings': 'wc-settings', }; if ( wcHandleMap[ request ] ) { return wcHandleMap[ request ]; @@ -59,11 +66,12 @@ const requestToHandle = ( request ) => { const CoreConfig = { ...baseConfig, entry: { - 'wc-shared-settings': './assets/js/settings/shared/index.js', - 'wc-block-settings': './assets/js/settings/blocks/index.js', + wcSettings: './assets/js/settings/shared/index.js', }, output: { - filename: '[name].js', + filename: ( chunkData ) => { + return `${ kebabCase( chunkData.chunk.name ) }.js`; + }, path: path.resolve( __dirname, './build/' ), library: [ 'wc', '[name]' ], libraryTarget: 'this', @@ -241,6 +249,7 @@ const GutenbergBlocksConfig = { requestToHandle, } ), ], + resolve: { alias }, }; const BlocksFrontendConfig = { @@ -326,6 +335,7 @@ const BlocksFrontendConfig = { requestToHandle, } ), ], + resolve: { alias }, }; module.exports = [ CoreConfig, GutenbergBlocksConfig, BlocksFrontendConfig ]; diff --git a/plugins/woocommerce-blocks/woocommerce-gutenberg-products-block.php b/plugins/woocommerce-blocks/woocommerce-gutenberg-products-block.php index 23f145b0eb2..673458ee84d 100644 --- a/plugins/woocommerce-blocks/woocommerce-gutenberg-products-block.php +++ b/plugins/woocommerce-blocks/woocommerce-gutenberg-products-block.php @@ -68,4 +68,49 @@ if ( is_readable( $autoloader ) ) { return; } -add_action( 'plugins_loaded', array( '\Automattic\WooCommerce\Blocks\Package', 'init' ) ); +/** + * Loads the dependency injection container for woocommerce blocks. + * + * @param boolean $reset Used to reset the container to a fresh instance. + * Note: this means all dependencies will be reconstructed. + */ +function wc_blocks_container( $reset = false ) { + static $container; + if ( + ! $container instanceof Automattic\WooCommerce\Blocks\Registry\Container + || $reset + ) { + $container = new Automattic\WooCommerce\Blocks\Registry\Container(); + // register Package. + $container->register( + Automattic\WooCommerce\Blocks\Domain\Package::class, + function ( $container ) { + return new Automattic\WooCommerce\Blocks\Domain\Package( + '2.5.0-dev', + __FILE__ + ); + } + ); + // register Bootstrap. + $container->register( + Automattic\WooCommerce\Blocks\Domain\Bootstrap::class, + function ( $container ) { + return new Automattic\WooCommerce\Blocks\Domain\Bootstrap( + $container + ); + } + ); + } + return $container; +} + +add_action( 'plugins_loaded', 'wc_blocks_bootstrap' ); +/** + * Boostrap WooCommerce Blocks App + */ +function wc_blocks_bootstrap() { + // initialize bootstrap. + wc_blocks_container()->get( + Automattic\WooCommerce\Blocks\Domain\Bootstrap::class + ); +}