g[] $handles * @param string[] $features See $ADVANCED_ENQUEUE_FEATURE_* constants or `null` for all features * @param string $type Can be `script` or `style` * @param string[] $preloadChunks Chunks to preload by name */ public function enableAdvancedEnqueue($handles, $features = null, $type = 'script', $preloadChunks = []) { $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, $preloadChunks); } if ($features === null || \in_array(Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_PRIORITY_QUEUE, $features, \true)) { $this->enablePriorityQueue($handles, $type); } } /** * Checks if a given feature is enabled for a given handle. * * @param string $handle * @param string $feature * @return boolean */ public function isAdvancedEnqueueEnabled($handle, $feature) { return isset($this->handleToFeatures[$handle]) && \in_array($feature, $this->handleToFeatures[$handle], \true); } /** * 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]; foreach ($handles as $handle) { $this->handleToFeatures[$handle] = \array_merge($this->handleToFeatures[$handle] ?? [], [Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_DEFER]); } \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]; foreach ($handles as $handle) { $this->handleToFeatures[$handle] = \array_merge($this->handleToFeatures[$handle] ?? [], [Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_ASYNC]); } \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` * @param string[] $preloadChunks Chunks to preload by name * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content */ public function enablePreloadEnqueue($handles, $type = 'script', $preloadChunks = []) { static $preloadedChunks = []; $handles = \is_array($handles) ? $handles : [$handles]; $wp_dependencies = $type === 'script' ? \wp_scripts() : \wp_styles(); foreach ($handles as $handle) { $this->handleToFeatures[$handle] = \array_merge($this->handleToFeatures[$handle] ?? [], [Constants::ASSETS_ADVANCED_ENQUEUE_FEATURE_PRELOADING]); } \add_action('wp_head', function () use($handles, $type, $wp_dependencies, $preloadChunks, &$preloadedChunks) { 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); // Add chunk preloads if desired $chunks = $wp_dependencies->get_data($handle, 'chunks'); if ($chunks) { foreach ($chunks as $chunkName => $chunkUrl) { if (!\in_array($chunkName, $preloadChunks, \true) || \in_array($chunkUrl, $preloadedChunks, \true)) { continue; } $chunkUrl = \apply_filters('script_loader_src', $chunkUrl, $handle); $preloadedChunks[] = $chunkUrl; \printf(' ', \esc_url($chunkUrl), 'script'); } } } } }, 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 a map of all entry chunks manifests for all entry points. */ public function getChunkEntryChunksManifest() { $path = \trailingslashit($this->getPluginConstant(Constants::PLUGIN_CONST_PATH)); static $chunkEntryChunksManifest = null; if ($chunkEntryChunksManifest === null) { $chunkEntryChunksManifest = []; $chunkEntryChunksManifestFiles = \glob($path . $this->getPublicFolder() . '*-entry-chunks-manifest.json'); if ($chunkEntryChunksManifestFiles !== \false) { foreach ($chunkEntryChunksManifestFiles as $chunkEntryChunksManifestFile) { $decoded = \json_decode(\file_get_contents($chunkEntryChunksManifestFile), ARRAY_A); if (\is_array($decoded)) { $chunkEntryChunksManifest = \array_merge($chunkEntryChunksManifest, $decoded); } } } } return $chunkEntryChunksManifest ?? []; } /** * 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; } /** * Enables a dummy handle which is enqueued in the footer. In general, this script is never loaded on the frontend * but it allows you to use the `$handle` for e.g. `wp_localize_script()`. It allows the following scenario: * * 1. Enqueue a `' . ($bypassJsonParse ? '' : ''), $uuid, \wp_json_encode($l10n), $object_name, \join(' ', [ // TODO: shouldn't this be part of @devowl-wp/cache-invalidate? // Compatibility with most caching plugins which lazy load JavaScript 'data-skip-lazy-load="js-extra"', // Compatibility with WP Fastest Cache and "Eliminate render blocking script" // as WPFC is moving all scripts (even with `type="text/plain"`). 'data-skip-moving="true"', // Compatibility with LiteSpeed Cache and do not delay this inline script // See https://github.com/litespeedtech/lscache_wp/blob/6c95240003b89ef1d4ce190f5a96eba83528cd89/src/optimize.cls.php#L903 'data-no-defer', // Compatibility with NitroPack 'nitro-exclude', // Compatibility with WP Rocket as the filter `rocket_defer_inline_exclusions` does only check on `