$this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_SLUG), 'textDomain' => $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_TEXT_DOMAIN), 'version' => $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_VERSION), 'restUrl' => $this->getAsciiUrl(Service::getUrl($this)), 'restNamespace' => Service::getNamespace($this), 'restRoot' => $this->getAsciiUrl(get_rest_url()), 'restQuery' => ['_v' => $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_VERSION)], 'restNonce' => wp_installing() && !is_multisite() ? '' : wp_create_nonce('wp_rest'), 'publicUrl' => trailingslashit( plugins_url('public', $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_FILE)) ), 'others' => $this->overrideLocalizeScript($context) ]; } /** * Enqueue react and react-dom (first check version is minium 16.8 so hooks work correctly). */ public function enqueueReact() { $useNonMinifiedSources = $this->useNonMinifiedSources(); $reactDev = 'react/umd/react.development.js'; $reactProd = 'react/umd/react.production.min.js'; $reactDomDev = 'react-dom/umd/react-dom.development.js'; $reactDomProd = 'react-dom/umd/react-dom.production.min.js'; $enqueueDom = true; // React (first check version is minium 16.8 so hooks work correctly) $coreReact = wp_scripts()->query(self::$HANDLE_REACT); if ($coreReact !== false && version_compare($coreReact->ver, '16.8', '<')) { // Replace the already existing React version with ours $publicFolder = $this->getPublicFolder(true); $devFileExists = file_exists( $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_PATH) . '/' . $publicFolder . $reactDev ); $publicSrc = $publicFolder . ($useNonMinifiedSources && $devFileExists ? $reactDev : $reactProd); $coreReact->src = plugins_url($publicSrc, $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_FILE)); // Also use our ReactDOM, instead we get this error: https://reactjs.org/docs/error-decoder.html/?invariant=321 $coreReactDom = wp_scripts()->query(self::$HANDLE_REACT_DOM); if ($coreReactDom !== false) { $publicSrc = $publicFolder . ($useNonMinifiedSources && $devFileExists ? $reactDomDev : $reactDomProd); $coreReactDom->src = plugins_url( $publicSrc, $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_FILE) ); $enqueueDom = false; } } else { // Enqueue our React version and let WP decide which version to take $this->enqueueLibraryScript(self::$HANDLE_REACT, [[$useNonMinifiedSources, $reactDev], $reactProd]); } if ($enqueueDom) { $this->enqueueLibraryScript( self::$HANDLE_REACT_DOM, [[$useNonMinifiedSources, $reactDomDev], $reactDomProd], self::$HANDLE_REACT ); } } /** * Enqueue utils and return an array of dependencies. */ public function enqueueUtils() { $this->enqueueReact(); $this->enqueueMobx(); $scriptDeps = [self::$HANDLE_REACT, self::$HANDLE_REACT_DOM, self::$HANDLE_MOBX, 'wp-i18n', 'jquery']; $handleUtils = $this->enqueueComposerScript('utils', $scriptDeps); array_push($scriptDeps, $handleUtils); return $scriptDeps; } /** * Enqueue mobx state management library. */ public function enqueueMobx() { $useNonMinifiedSources = $this->useNonMinifiedSources(); $this->enqueueLibraryScript(self::$HANDLE_MOBX, [ [$useNonMinifiedSources, 'mobx/lib/mobx.umd.js'], 'mobx/lib/mobx.umd.min.js' ]); } /** * Checks if a `vendor-` file is created for a given script and enqueue it. * * @param string $handle * @param boolean $isLib * @param string $src * @param string[] $deps * @param boolean $in_footer * @param string $media */ protected function probablyEnqueueChunk($handle, $isLib, $src, &$deps, $in_footer, $media) { if (!$isLib) { $handle = $this->enqueue('vendor-' . $handle, 'vendor-' . $src, $deps, false, 'script', $in_footer, $media); if ($handle !== false) { array_push($deps, $handle); } } } /** * Enqueue helper for entry points and libraries. See dependents for more documentation. * * @param string $handle * @param mixed $src * @param string[] $deps * @param boolean $isLib * @param string $type Can be 'script' or 'style' * @param boolean $in_footer * @param string $media * @return string|boolean The used handle */ protected function enqueue( $handle, $src, $deps = [], $isLib = false, $type = 'script', $in_footer = true, $media = 'all' ) { $useHandle = $isLib ? $handle : $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_SLUG) . '-' . $handle; if (!is_array($src)) { $src = [$src]; } $publicFolder = $this->getPublicFolder($isLib); foreach ($src as $s) { // Allow to skip e. g. SCRIPT_DEBUG files if (is_array($s) && $s[0] !== true) { continue; } $useSrc = is_array($s) ? $s[1] : $s; $publicSrc = $publicFolder . $useSrc; $path = path_join($this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_PATH), $publicSrc); if (file_exists($path)) { $url = plugins_url($publicSrc, $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_FILE)); $cachebuster = $this->getCachebusterVersion($publicSrc, $isLib); if ($type === 'script') { $this->probablyEnqueueChunk($useHandle, $isLib, $useSrc, $deps, $in_footer, $media); wp_enqueue_script($useHandle, $url, $deps, $cachebuster, $in_footer); // Only set translations for our own entry points, libraries handle localization usually in another way if (!$isLib) { $this->setLazyScriptTranslations( $useHandle, $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_TEXT_DOMAIN), path_join( $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_PATH), self::$PUBLIC_JSON_I18N ) ); } } else { wp_enqueue_style($useHandle, $url, $deps, $cachebuster, $media); } return $useHandle; } } return false; } /** * Registers the script if $src provided (does NOT overwrite), and enqueues it. Use this wrapper * method instead of wp_enqueue_script if you want to use the cachebuster for the given src. If the * src is not found in the cachebuster (inc/base/others/cachebuster.php) it falls back to _VERSION. * * You can also use something like this to determine SCRIPT_DEBUG files: * * ```php * $this->enqueueLibraryScript( * Assets::$HANDLE_REACT_DOM, * [[$useNonMinifiedSources, 'react-dom/umd/react-dom.development.js'], 'react-dom/umd/react-dom.production.min.js'], * Assets::$HANDLE_REACT * ); * ``` * * @param string $handle Name of the script. Should be unique. * @param mixed $src The src relative to public/dist or public/dev folder (when $isLib is false) * @param string[] $deps An array of registered script handles this script depends on. * @param boolean $in_footer Whether to enqueue the script before instead of in the . * @param boolean $isLib If true the public/lib/ folder is used. * @return string|boolean The used handle * @see https://developer.wordpress.org/reference/functions/wp_enqueue_script/ For parameters */ public function enqueueScript($handle, $src, $deps = [], $in_footer = true, $isLib = false) { return $this->enqueue($handle, $src, $deps, $isLib, 'script', $in_footer); } /** * Wrapper for Assets#enqueueScript() method with $isLib = true. * * @param string $handle * @param mixed $src * @param string[] $deps * @param boolean $in_footer * @return string|boolean The used handle * @see self::enqueueScript() */ public function enqueueLibraryScript($handle, $src, $deps = [], $in_footer = false) { return $this->enqueueScript($handle, $src, $deps, $in_footer, true); } /** * Enqueue a CSS stylesheet. Use this wrapper method instead of wp_enqueue_style if you want * to use the cachebuster for the given src. If the src is not found in the cachebuster (inc/base/others/cachebuster.php) * it falls back to _VERSION. * * It also allows $src to be like in enqueueScript() * * @param string $handle Name of the style. Should be unique. * @param mixed $src The src relative to public/dist or public/dev folder (when $isLib is false) * @param string[] $deps An array of registered style handles this style depends on. * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @param boolean $isLib If true the public/lib/ folder is used. * @return string|boolean The used handle * @see https://developer.wordpress.org/reference/functions/wp_enqueue_style/ For parameters */ public function enqueueStyle($handle, $src, $deps = [], $media = 'all', $isLib = false) { return $this->enqueue($handle, $src, $deps, $isLib, 'style', null, $media); } /** * Wrapper for Assets#enqueueStyle() method with $isLib = true. * * @param string $handle * @param mixed $src * @param string[] $deps * @param string $media * @return string|boolean The used handle * @see enqueueStyle() */ public function enqueueLibraryStyle($handle, $src, $deps = [], $media = 'all') { return $this->enqueueStyle($handle, $src, $deps, $media, true); } /** * Checks if a `vendor-` file is created for a given script in a composer package and enqueue it. * * @param string $handle * @param string $src * @param string[] $deps * @param boolean $in_footer * @param string $media */ protected function probablyEnqueueComposerChunk($handle, $src, &$deps, $in_footer, $media) { $rootSlug = $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_ROOT_SLUG); $scriptSuffix = $src === 'index.js' || $src === 'index.css' ? '' : '-' . pathinfo($src, PATHINFO_FILENAME); $handle = $this->enqueueComposer( $handle, 'vendor-' . $src, $deps, 'script', $in_footer, $media, 'vendor-' . $rootSlug . '-' . $handle . $scriptSuffix ); if ($handle !== false) { array_push($deps, $handle); } } /** * Enqueue helper for monorepo packages. See dependents for more documentation. * * @param string $handle * @param string $src * @param string[] $deps * @param string $type Can be 'script' or 'style' * @param boolean $in_footer * @param string $media * @param string $vendorHandle * @return string|boolean The used handle */ protected function enqueueComposer( $handle, $src = 'index.js', $deps = [], $type = 'script', $in_footer = true, $media = 'all', $vendorHandle = null ) { $rootSlug = $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_ROOT_SLUG); $pluginPath = $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_PATH); $scriptSuffix = $src === 'index.js' || $src === 'index.css' ? '' : '-' . pathinfo($src, PATHINFO_FILENAME); $useHandle = $vendorHandle !== null ? $vendorHandle : $rootSlug . '-' . $handle . $scriptSuffix; $useNonMinifiedSources = $this->useNonMinifiedSources(); $packageDir = 'vendor/' . $rootSlug . '/' . $handle . '/'; $devBundlesExists = is_dir($pluginPath . '/' . $packageDir . 'dev'); $packageSrc = $packageDir . ($useNonMinifiedSources && $devBundlesExists ? 'dev' : 'dist') . '/' . $src; $composerPath = path_join($pluginPath, $packageSrc); $isInLernaRepo = file_exists(path_join(WP_CONTENT_DIR, 'packages/' . $handle . '/tsconfig.json')); if (file_exists($composerPath)) { // The lerna package exists (we are in our local development environment!) $url = plugins_url($packageSrc, $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_FILE)); $cachebuster = filemtime($composerPath); $packageJson = path_join($pluginPath, $packageDir . 'package.json'); // Correct cachebuster version if (file_exists($packageJson) && !$isInLernaRepo) { $packageJson = json_decode(file_get_contents($packageJson), true); $cachebuster = $packageJson['version']; } if ($type === 'script') { $this->probablyEnqueueComposerChunk($handle, $src, $deps, $in_footer, $media); wp_enqueue_script($useHandle, $url, $deps, $cachebuster, $in_footer); $this->setLazyScriptTranslations( $useHandle, $rootSlug . '-' . $handle, path_join($pluginPath, $packageDir . 'languages/frontend/json') ); } else { wp_enqueue_style($useHandle, $url, $deps, $cachebuster, $media); } return $useHandle; } return false; } /** * Enqueue a composer package script from our multi-package repository. * * @param string $handle Name of the package. * @param string[] $deps An array of registered scripts handles this script depends on. * @param string $src The file to use in dist or dev folder. * @param boolean $in_footer Whether to enqueue the script before instead of in the . * @return string The used handle */ public function enqueueComposerScript($handle, $deps = [], $src = 'index.js', $in_footer = true) { return $this->enqueueComposer($handle, $src, $deps, 'script', $in_footer); } /** * Enqueue a composer package style from our multi-package repository. * * @param string $handle Name of the package. * @param string[] $deps An array of registered scripts handles this script depends on. * @param string $src The file to use in dist or dev folder. * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. * @return string The used handle */ public function enqueueComposerStyle($handle, $deps = [], $src = 'index.css', $media = 'all') { return $this->enqueueComposer($handle, $src, $deps, 'style', null, $media); } /** * Enqueue scripts and styles for admin pages. * * @param string $hook_suffix The current admin page */ public function admin_enqueue_scripts($hook_suffix) { $this->enqueue_scripts_and_styles(self::$TYPE_ADMIN, $hook_suffix); } /** * Enqueue scripts and styles for frontend pages. */ public function wp_enqueue_scripts() { $this->enqueue_scripts_and_styles(self::$TYPE_FRONTEND); } /** * The function and mechanism of wp_set_script_translations() is great of course. Unfortunately * popular plugins like WP Rocket and Divi are not compatible with it (especially page builders * and caching plugins). Why? Shortly explained, the injected inline scripts relies on `wp.i18n` * which can be deferred or async loaded (the script itself) -> wp is not defined. * * In factory i18n.tsx the `window.wpi18nLazy` is automatically detected and the plugin gets localized. * * @param string $handle * @param string $domain * @param string $path * @see https://developer.wordpress.org/reference/classes/wp_scripts/print_translations/ * @see https://developer.wordpress.org/reference/functions/wp_set_script_translations/ * @see https://app.clickup.com/t/3mjh0e */ public function setLazyScriptTranslations($handle, $domain, $path) { $json_translations = load_script_textdomain($handle, $domain, $path); if (!empty($json_translations)) { $output = <<getPluginConstant(PluginReceiver::$PLUGIN_CONST_VERSION); $path = $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_INC) . '/base/others/'; $path_lib = $path . 'cachebuster-lib.php'; $path = $path . 'cachebuster.php'; if ($isLib) { // Library cachebuster if (file_exists($path_lib)) { static $cachebuster_lib = null; if ($cachebuster_lib === null) { $cachebuster_lib = include $path_lib; } // Parse module preg_match(self::$LIB_CACHEBUSTER_REGEX, $src, $matches); if ( is_array($matches) && isset($matches[1]) && ($module = $matches[1]) && is_array($cachebuster_lib) && array_key_exists($module, $cachebuster_lib) ) { // Valid cachebuster return $cachebuster_lib[$module]; } } } else { // Main cachebuster if (file_exists($path)) { // Store cachebuster once static $cachebuster = null; if ($cachebuster === null) { $cachebuster = include $path; } // Prepend src/ because the Grunt cachebuster prefixes it $src = 'src/' . $src; if (is_array($cachebuster) && array_key_exists($src, $cachebuster)) { // Valid cachebuster return $cachebuster[$src]; } } } return $default; } /** * Wrapper for plugins_url. It respects the public folder depending on the SCRIPTS_DEBUG constant. * * @param string $asset The file name relative to the public folder path (dist or dev) * @param boolean $isLib If true the public/lib/ folder is used. * @return string * @see getPublicFolder() */ public function getPluginsUrl($asset, $isLib = false) { return plugins_url( $this->getPublicFolder($isLib) . $asset, $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_FILE) ); } /** * Gets a public folder depending on the debug mode relative to the plugins folder with trailing slash. * * @param boolean $isLib If true the public/lib/ folder is returned. * @return string */ public function getPublicFolder($isLib = false) { if ($isLib) { return 'public/lib/'; } elseif ($this->useNonMinifiedSources()) { // Check if dev folder exists because it can be removed in some builds $path = $this->getPluginConstant(PluginReceiver::$PLUGIN_CONST_PATH); if (is_dir($path . '/public/dev') || is_dir($path . '/src/public/dev') /* monorepo development */) { return 'public/dev/'; } } return 'public/dist/'; } /** * Convert a complete URL to IDN url. This is necessery if you use a URIBuilder like * lil-url in your frontend. * * @see https://www.php.net/manual/en/function.idn-to-ascii.php * @param string $url The url * @return string * @codeCoverageIgnore Completely relies on external library! */ public function getAsciiUrl($url) { if (!class_exists('Requests_IRI')) { require_once ABSPATH . WPINC . '/Requests/IRI.php'; } if (!class_exists('Requests_IDNAEncoder')) { require_once ABSPATH . WPINC . '/Requests/IDNAEncoder.php'; } $iri = new Requests_IRI($url); $iri->host = Requests_IDNAEncoder::encode($iri->host); return $iri->uri; } /** * Check if SCRIPT_DEBUG is set to true. * * @return boolean */ public function useNonMinifiedSources() { return defined('SCRIPT_DEBUG') && constant('SCRIPT_DEBUG') === true; } /** * Checks if a specific screen is active. * * @param string $base The base * @return boolean */ public function isScreenBase($base) { if (function_exists('get_current_screen')) { $screen = get_current_screen(); } else { return false; } if (isset($screen->base)) { return $screen->base === $base; } else { return false; } } }