script` or `style` */ public function enableAdvancedEnqueue($handles, $features = null, $type = 'script') { $handles = \is_array($handles) ? $handles : [$handles]; // Add `vendor-` also to the handles for `probablyEnqueueChunk` compatibility foreach ($handles as $handle) { \array_unshift($handles, \sprintf('vendor-%s', $handle)); \array_unshift($handles, \sprintf('%s-vendor-%s', $this->getPluginConstant(Constants::PLUGIN_CONST_SLUG), $handle)); } if (($features === null || \in_array(Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_DEFER, $features, \true)) && $type === 'script') { $this->enableDeferredEnqueue($handles); } if (($features === null || \in_array(Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_ASYNC, $features, \true)) && $type === 'script') { $this->enableAsyncEnqueue($handles); } if ($features === null || \in_array(Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_PRELOADING, $features, \true)) { $this->enablePreloadEnqueue($handles, $type); } if ($features === null || \in_array(Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_PRIORITY_QUEUE, $features, \true)) { $this->enablePriorityQueue($handles, $type); } } /** * Enable `defer` attribute for given handle(s) (only scripts are supported, see https://stackoverflow.com/a/25890780). * * @param string|string[] $handles * @see https://stackoverflow.com/a/56128726/5506547 */ public function enableDeferredEnqueue($handles) { $handles = \is_array($handles) ? $handles : [$handles]; \add_filter('script_loader_tag', function ($tag, $handle) use($handles) { if (\in_array($handle, $handles, \true) && \stripos($tag, 'defer') === \false) { // see https://regex101.com/r/0whi5s/1 // phpcs:disable PHPCompatibility.ParameterValues.RemovedPCREModifiers.Removed return \preg_replace(\sprintf('/(%s=[\'"]?)/m', 'src'), 'defer $1', $tag); // phpcs:enable PHPCompatibility.ParameterValues.RemovedPCREModifiers.Removed } return $tag; }, 10, 2); } /** * Enable `async` attribute for given handle(s) (only scripts are supported). * * @param string|string[] $handles */ public function enableAsyncEnqueue($handles) { $handles = \is_array($handles) ? $handles : [$handles]; \add_filter('script_loader_tag', function ($tag, $handle) use($handles) { if (\in_array($handle, $handles, \true) && \stripos($tag, 'async') === \false) { // see https://regex101.com/r/0whi5s/1 // phpcs:disable PHPCompatibility.ParameterValues.RemovedPCREModifiers.Removed return \preg_replace(\sprintf('/(%s=[\'"]?)/m', 'src'), 'async $1', $tag); // phpcs:enable PHPCompatibility.ParameterValues.RemovedPCREModifiers.Removed } return $tag; }, 10, 2); } /** * Enable `` HTML tag for given handle(s). * * @param string|string[] $handles * @param string $type Can be `script` or `style` * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content */ public function enablePreloadEnqueue($handles, $type = 'script') { $handles = \is_array($handles) ? $handles : [$handles]; $wp_dependencies = $type === 'script' ? \wp_scripts() : \wp_styles(); \add_action('wp_head', function () use($handles, $type, $wp_dependencies) { foreach ($handles as $handle) { $script = $wp_dependencies->query($handle); if ($script !== \false) { // Build URL // See https://developer.wordpress.org/reference/classes/wp_styles/do_item/ // and https://developer.wordpress.org/reference/classes/wp_scripts/do_item/ $src = $script->src; $ver = $script->ver; if (!empty($ver)) { $src = \add_query_arg('ver', $ver, $src); } $src = \apply_filters('script_loader_src', $src, $handle); \printf(' ', \esc_url($src), $type); } } }, 2); } /** * Enable scripts and styles to be appear at the top of `
query($handle); if ($script !== \false) { if ($wp_dependencies->do_item($handle, \false)) { $wp_dependencies->done[] = $handle; } unset($wp_dependencies->to_do[$handle]); } } }, 3); } /** * Get a map of available translations for all available chunks. */ public function getChunkTranslationMap() { $inc = $this->getPluginConstant(Constants::PLUGIN_CONST_INC); $path = $inc . '/base/others/cachebuster.php'; $result = []; if (empty($inc)) { // There is no `inc` folder available for the current package / plugin. return $result; } if (\file_exists($path)) { // Store cachebuster once static $cachebuster = null; if ($cachebuster === null) { $cachebuster = (include $path); } foreach (\array_keys($cachebuster) as $scriptPath) { $basename = \basename($scriptPath); if (\substr($basename, 0, 6) === 'chunk-') { $suffix = $this->getTranslationSuffixByBasename($basename); if (\count($suffix) > 0) { $result[$basename] = $suffix; } } } } return (object) $result; } /** * Get the suffix for `chunks` localized variable including dependencies. * * @param string $basename */ protected function getTranslationSuffixByBasename($basename) { $result = []; static $locale = null; if ($locale === null) { $locale = \determine_locale(); } $textDomain = $this->getPluginConstant(Constants::PLUGIN_CONST_TEXT_DOMAIN); $path = \trailingslashit($this->getPluginConstant(Constants::PLUGIN_CONST_PATH)); $languageFolder = PackageLocalization::getParentLanguageFolder($path . Constants::LOCALIZATION_PUBLIC_JSON_I18N); static $dependencyMap = null; if ($dependencyMap === null) { $dependencyMap = []; $dependencyMapFiles = \glob($path . $this->getPublicFolder() . 'i18n-dependency-map-*.json'); if ($dependencyMapFiles !== \false) { foreach ($dependencyMapFiles as $dependencyMapFile) { $dependencyMap = \array_merge($dependencyMap, \json_decode(\file_get_contents($dependencyMapFile), ARRAY_A)); } } } // Chunk entry dependencies if (isset($dependencyMap[$basename])) { $dependencies = $dependencyMap[$basename]; foreach ($dependencies as $dependency) { $suffix = $locale . '-' . \md5($dependency); $jsonFile = $languageFolder . $textDomain . '-' . $suffix . '.json'; if (\file_exists($jsonFile)) { $result[] = $suffix; } } } return $result; } /** * When using WordPress < 6.6 we need to enqueue the react/jsx-runtime UMD bundle to make the * `ReactJSXRuntime` external work. * * @see https://make.wordpress.org/core/2024/06/06/jsx-in-wordpress-6-6/ * @see https://core.trac.wordpress.org/ticket/61324 * @see https://github.com/WordPress/gutenberg/issues/62202#issuecomment-2156796649 * @see https://app.clickup.com/t/86959qqq1?comment=90120059024238 */ public function enqueueReactJsxRuntime() { if (\wp_script_is(Constants::ASSETS_HANDLE_REACT_JSX_RUNTIME, 'registered')) { return; } $useNonMinifiedSources = $this->useNonMinifiedSources(); $this->enqueueLibraryScript(Constants::ASSETS_HANDLE_REACT_JSX_RUNTIME, [[$useNonMinifiedSources, 'react-jsx-runtime/react-jsx-runtime.js'], 'react-jsx-runtime/react-jsx-runtime.min.js'], [Constants::ASSETS_HANDLE_REACT]); } /** * Enqueue utils and return an array of dependencies. */ public function enqueueUtils() { $this->enqueueMobx(); $this->enqueueReactJsxRuntime(); $scriptDeps = [Constants::ASSETS_HANDLE_REACT, Constants::ASSETS_HANDLE_REACT_DOM, Constants::ASSETS_HANDLE_REACT_JSX_RUNTIME, Constants::ASSETS_HANDLE_MOBX, 'wp-i18n', 'jquery', 'wp-polyfill']; $handleUtils = $this->enqueueComposerScript('utils', $scriptDeps); \array_push($scriptDeps, $handleUtils); return $scriptDeps; } /** * Enqueue mobx state management library. */ public function enqueueMobx() { $useNonMinifiedSources = $this->useNonMinifiedSources(); $alreadyRegistered = \wp_script_is(Constants::ASSETS_HANDLE_MOBX, 'registered'); $this->enqueueLibraryScript(Constants::ASSETS_HANDLE_MOBX, [[$useNonMinifiedSources, 'mobx/lib/mobx.umd.js'], 'mobx/lib/mobx.umd.min.js']); // Add inline script only once (this allows multiple instances in mobx) if (!$alreadyRegistered) { \wp_add_inline_script(Constants::ASSETS_HANDLE_MOBX, 'document.addEventListener("DOMContentLoaded", function () { mobx.configure({ isolateGlobalState: true }); });'); } } /** * 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(Constants::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 = \trailingslashit($this->getPluginConstant(Constants::PLUGIN_CONST_PATH)) . $publicSrc; if (\file_exists($path)) { $url = \plugins_url($publicSrc, $this->getPluginConstant(Constants::PLUGIN_CONST_FILE)); // In dev environment, the cachebuster is not created; it is only created at build time $isDevEnv = \defined('DEVOWL_WP_DEV') && \constant('DEVOWL_WP_DEV'); $cachebuster = $this->getCachebusterVersion($publicSrc, $isLib, $isDevEnv ? \filemtime($path) : null); // Never overwrite e.g. `react` or `mobx` // TODO: perhaps provide another function `$this->allowDeregisterWhenNewVersion($handle)` // $this->probablyDeregisterWhenNewerVersion($useHandle, $cachebuster, $type); 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(Constants::PLUGIN_CONST_TEXT_DOMAIN), \trailingslashit($this->getPluginConstant(Constants::PLUGIN_CONST_PATH)) . Constants::LOCALIZATION_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( * Constants::ASSETS_HANDLE_REACT_DOM, * [[$useNonMinifiedSources, 'react-dom/umd/react-dom.development.js'], 'react-dom/umd/react-dom.production.min.js'], * Constants::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(Constants::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(Constants::PLUGIN_CONST_ROOT_SLUG); $pluginPath = \trailingslashit($this->getPluginConstant(Constants::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 = $pluginPath . $packageSrc; $isLocalDevelopment = \defined('DEVOWL_WP_DEV') && \constant('DEVOWL_WP_DEV'); if (\file_exists($composerPath)) { // The lerna package exists (we are in our local development environment!) $url = \plugins_url($packageSrc, $this->getPluginConstant(Constants::PLUGIN_CONST_FILE)); $cachebuster = \filemtime($composerPath); $packageJson = $pluginPath . $packageDir . 'package.json'; // Correct cachebuster version if (\file_exists($packageJson) && !$isLocalDevelopment) { $packageJson = \json_decode(\file_get_contents($packageJson), \true); $cachebuster = $packageJson['version']; } $this->probablyDeregisterWhenNewerVersion($useHandle, $cachebuster, $type); 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, $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(Constants::ASSETS_TYPE_ADMIN, $hook_suffix); } /** * Enqueue scripts and styles for frontend pages. */ public function wp_enqueue_scripts() { $this->enqueue_scripts_and_styles(Constants::ASSETS_TYPE_FRONTEND); } /* * Enqueue blocker and banner in Login screen too, so reCaptcha forms or * similar scripts can be blocked. */ public function login_enqueue_scripts() { $this->enqueue_scripts_and_styles(Constants::ASSETS_TYPE_LOGIN); } /** * Enqueue scripts in customize (not preview!) */ public function customize_controls_print_scripts() { $this->enqueue_scripts_and_styles(Constants::ASSETS_TYPE_CUSTOMIZE); } /** * 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) { \add_filter('load_script_textdomain_relative_path', [$this, 'load_script_textdomain_relative_path']); \add_filter('load_script_translation_file', [$this, 'load_script_translation_file']); $json_translations = \load_script_textdomain($handle, $domain, PackageLocalization::getParentLanguageFolder($path)); \remove_filter('load_script_textdomain_relative_path', [$this, 'load_script_textdomain_relative_path']); \remove_filter('load_script_translation_file', [$this, 'load_script_translation_file']); if (!empty($json_translations)) { $output = <<