$manifest, 'dir' => $manifest_dir, 'is_dev' => $is_dev, ]; return $manifests[$manifest_path]; } /** * Filter script tag * * This creates a function to be used as callback for the `script_loader` filter * which adds `type="module"` attribute to the script tag. * * @since 0.1.0 * * @param string $handle Script handle. */ public function filter_script_tag(string $handle): void { add_filter('script_loader_tag', fn (...$args) => $this->set_script_type_attribute($handle, ...$args), 10, 3); } /** * Add `type="module"` to a script tag * * @since 0.1.0 * @since 0.8.0 Use WP_HTML_Tag_Processor. * * @param string $target_handle Handle of the script being targeted by the filter callback. * @param string $tag Original script tag. * @param string $handle Handle of the script that's currently being filtered. * @param string $src The sript src. * @return string Script tag with attribute `type="module"` added. */ public function set_script_type_attribute(string $target_handle, string $tag, string $handle, string $src): string { if ($target_handle !== $handle) { return $tag; } $processor = new WP_HTML_Tag_Processor($tag); $script_found = false; do { $script_found = $processor->next_tag('script'); } while ($processor->get_attribute('src') !== $src); if ($script_found) { $processor->set_attribute('type', 'module'); } return $processor->get_updated_html(); } /** * Generate development asset src * * @since 0.1.0 * * @param object $manifest Asset manifest. * @param string $entry Asset entry name. */ public function generate_development_asset_src(object $manifest, string $entry): string { return sprintf( '%s/%s', untrailingslashit($manifest->data->origin), trim(preg_replace('/[\/]{2,}/', '/', "{$manifest->data->base}/{$entry}"), '/') ); } /** * Register vite client script * * @since 0.1.0 * * @param object $manifest Asset manifest. */ public function register_vite_client_script(object $manifest): void { if (wp_script_is(self::VITE_CLIENT_SCRIPT_HANDLE)) { return; } $src = $this->generate_development_asset_src($manifest, '@vite/client'); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script(self::VITE_CLIENT_SCRIPT_HANDLE, $src, [], null, false); $this->filter_script_tag(self::VITE_CLIENT_SCRIPT_HANDLE); } /** * Inject react-refresh preamble script once, if needed * * @since 0.8.0 * * @param object $manifest Asset manifest. */ public function inject_react_refresh_preamble_script(object $manifest): void { static $is_react_refresh_preamble_printed = false; if ($is_react_refresh_preamble_printed) { return; } if (! in_array('vite:react-refresh', $manifest->data->plugins, true)) { return; } $react_refresh_script_src = $this->generate_development_asset_src($manifest, '@react-refresh'); $script_position = 'after'; $script = ' import RefreshRuntime from "'.$react_refresh_script_src.'"; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true;'; wp_add_inline_script(self::VITE_CLIENT_SCRIPT_HANDLE, $script, $script_position); add_filter( 'wp_inline_script_attributes', function (array $attributes) use ($script_position): array { if (isset($attributes['id']) && $attributes['id'] === self::VITE_CLIENT_SCRIPT_HANDLE."-js-{$script_position}") { $attributes['type'] = 'module'; } return $attributes; } ); $is_react_refresh_preamble_printed = true; } /** * Load development asset * * @since 0.1.0 * * @param object $manifest Asset manifest. * @param string $entry Entrypoint to enqueue. * @param array $options Enqueue options. * @return array|null Array containing registered scripts or NULL if the none was registered. */ public function load_development_asset(object $manifest, string $entry, array $options): ?array { $this->register_vite_client_script($manifest); $this->inject_react_refresh_preamble_script($manifest); $dependencies = array_merge( [self::VITE_CLIENT_SCRIPT_HANDLE], $options['dependencies'] ); $src = $this->generate_development_asset_src($manifest, $entry); $this->filter_script_tag($options['handle']); // This is a development script, browsers shouldn't cache it. // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion if (! wp_register_script($options['handle'], $src, $dependencies, null, $options['in-footer'])) { return null; } $assets = [ 'scripts' => [$options['handle']], 'styles' => $options['css-dependencies'], ]; /** * Filter registered development assets * * @param array $assets Registered assets. * @param object $manifest Manifest object. * @param string $entry Entrypoint file. * @param array $options Enqueue options. */ $assets = apply_filters('agentwp_vite__development_assets', $assets, $manifest, $entry, $options); return $assets; } /** * Load production asset * * @since 0.1.0 * * @param object $manifest Asset manifest. * @param string $entry Entrypoint to enqueue. * @param array $options Enqueue options. * @return array|null Array containing registered scripts & styles or NULL if there was an error. */ public function load_production_asset(object $manifest, string $entry, array $options): ?array { $url = $this->prepare_asset_url($manifest->dir); if (! isset($manifest->data->{$entry})) { if (defined('WP_DEBUG') && WP_DEBUG) { // Translators: %1$s is the entry point name. wp_die(esc_html(sprintf(__('[Vite] Entry %1$s not found.', 'agentwp'), $entry))); } return null; } $assets = [ 'scripts' => [], 'styles' => [], ]; $item = $manifest->data->{$entry}; $src = "{$url}/{$item->file}"; if (! $options['css-only']) { $this->filter_script_tag($options['handle']); // Don't worry about browser caching as the version is embedded in the file name. // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion if (wp_register_script($options['handle'], $src, $options['dependencies'], null, $options['in-footer'])) { $assets['scripts'][] = $options['handle']; } } if (! empty($item->imports)) { // Recursive inline function to deeply check for .css files. $check_imports = function (array $imports) use (&$check_imports, &$assets, $manifest, $url, $options): void { foreach ($imports as $import) { $import_item = $manifest->data->{$import}; if (! empty($import_item->imports)) { $check_imports($import_item->imports); } if (! empty($import_item->css)) { $this->register_stylesheets($assets, $import_item->css, $url, $options); } } }; $check_imports($item->imports); } if (! empty($item->css)) { $this->register_stylesheets($assets, $item->css, $url, $options); } /** * Filter registered production assets * * @param array $assets Registered assets. * @param object $manifest Manifest object. * @param string $entry Entrypoint file. * @param array $options Enqueue options. */ $assets = apply_filters('agentwp_vite__production_assets', $assets, $manifest, $entry, $options); return $assets; } /** * Register stylesheet assets to WordPress and saves stylesheet handles for later enqueuing * * @param array $assets Reference to registered assets. * @param array $stylesheets List of stylesheets to register. * @param string $url Base URL to asset. * @param array $options Array of options. */ public function register_stylesheets(array &$assets, array $stylesheets, string $url, array $options): void { foreach ($stylesheets as $css_file_path) { $slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', pathinfo($css_file_path, PATHINFO_FILENAME)), '-')); // Including a slug based on the actual css file in the handle ensures it wont be registered more than once. $style_handle = "{$options['handle']}-{$slug}"; // Don't worry about browser caching as the version is embedded in the file name. // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion if (wp_register_style($style_handle, "{$url}/{$css_file_path}", $options['css-dependencies'], null, $options['css-media'])) { $assets['styles'][] = $style_handle; } } } /** * Parse register/enqueue options * * @since 0.1.0 * * @param array $options Array of options. * @return array Array of options merged with defaults. */ public function parse_options(array $options): array { $defaults = [ 'css-dependencies' => [], 'css-media' => 'all', 'css-only' => false, 'dependencies' => [], 'handle' => '', 'in-footer' => false, ]; return wp_parse_args($options, $defaults); } /** * Prepare asset url * * @author Justin Slamka * * @since 0.4.0 * @since 0.6.1 Normalize paths so they work on Windows as well. * * @param string $dir Asset directory. * @return string */ public function prepare_asset_url(string $dir) { $content_dir = wp_normalize_path(WP_CONTENT_DIR); $manifest_dir = wp_normalize_path($dir); $url = content_url(str_replace($content_dir, '', $manifest_dir)); $url_matches_pattern = preg_match('/(?
http(?:s?):\/\/.*\/)(?wp-content(?\/.*)\/(?:plugins|themes)\/.*)/', $url, $url_parts); if ($url_matches_pattern === 0) { return $url; } ['address' => $address, 'fullPath' => $full_path, 'removablePath' => $removable_path] = $url_parts; return sprintf('%s%s', $address, str_replace($removable_path, '', $full_path)); } /** * Register asset * * @since 0.1.0 * @see load_development_asset * @see load_production_asset * * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`. * @param string $entry Entrypoint to enqueue. * @param array $options Enqueue options. */ public function register_asset(string $manifest_dir, string $entry, array $options): ?array { try { $manifest = $this->get_manifest($manifest_dir); } catch (Exception $e) { if (defined('WP_DEBUG') && WP_DEBUG) { wp_die(esc_html($e->getMessage())); } return null; } $options = $this->parse_options($options); $assets = $manifest->is_dev ? $this->load_development_asset($manifest, $entry, $options) : $this->load_production_asset($manifest, $entry, $options); return $assets; } /** * Enqueue asset * * @since 0.1.0 * @see register_asset * * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`. * @param string $entry Entrypoint to enqueue. * @param array $options Enqueue options. */ public function enqueue_asset(string $manifest_dir, string $entry, array $options): bool { $assets = $this->register_asset($manifest_dir, $entry, $options); if (is_null($assets)) { return false; } $map = [ 'scripts' => 'wp_enqueue_script', 'styles' => 'wp_enqueue_style', ]; foreach ($assets as $group => $handles) { $func = $map[$group]; foreach ($handles as $handle) { $func($handle); } } return true; } }