$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. * * @return void */ function filter_script_tag(string $handle): void { add_filter('script_loader_tag', fn(...$args) => 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. * * @return string Script tag with attribute `type="module"` added. */ function set_script_type_attribute(string $target_handle, string $tag, string $handle): string { if ($target_handle !== $handle) { return $tag; } $processor = new WP_HTML_Tag_Processor($tag); if ($processor->next_tag('script')) { $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. * * @return string */ 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. * * @return void */ function register_vite_client_script(object $manifest): void { if (wp_script_is(VITE_CLIENT_SCRIPT_HANDLE)) { return; } $src = generate_development_asset_src($manifest, '@vite/client'); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion wp_register_script(VITE_CLIENT_SCRIPT_HANDLE, $src, array(), null, false); filter_script_tag(VITE_CLIENT_SCRIPT_HANDLE); } /** * Inject react-refresh preamble script once, if needed * * @since 0.8.0 * * @param object $manifest Asset manifest. * @return void */ 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 = generate_development_asset_src($manifest, '@react-refresh'); $script_position = 'after'; $script = sprintf( 'if (!window.__vite_plugin_react_preamble_installed__) { import("%s").then((mod) => { const RefreshRuntime = mod.default; RefreshRuntime.injectIntoGlobalHook(window); window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true; }); }', esc_js($react_refresh_script_src) ); wp_add_inline_script(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'] === 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. */ function load_development_asset(object $manifest, string $entry, array $options): ?array { register_vite_client_script($manifest); inject_react_refresh_preamble_script($manifest); $dependencies = array_merge( array(VITE_CLIENT_SCRIPT_HANDLE), $options['dependencies'] ); $src = generate_development_asset_src($manifest, $entry); 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 = array( 'scripts' => array($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. */ 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. */ function load_production_asset(object $manifest, string $entry, array $options): ?array { $url = prepare_asset_url($manifest->dir); if (! isset($manifest->data->{$entry})) { if (defined('WP_DEBUG') && WP_DEBUG) { wp_die(esc_html(sprintf('[Vite] Entry %s not found.', $entry))); } return null; } $assets = array( 'scripts' => array(), 'styles' => array(), ); $item = $manifest->data->{$entry}; $src = "{$url}/{$item->file}"; if (! $options['css-only']) { 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->css)) { foreach ($item->css as $index => $css_file_path) { $style_handle = "{$options['handle']}-{$index}"; // 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; } } } /** * Filter registered production assets * * @param array $assets Registered assets. * @param object $manifest Manifest object. * @param string $entry Entrypoint file. * @param array $options Enqueue options. */ return $assets; } /** * Parse register/enqueue options * * @since 0.1.0 * * @param array $options Array of options. * * @return array Array of options merged with defaults. */ function parse_options(array $options): array { $defaults = array( 'css-dependencies' => array(), 'css-media' => 'all', 'css-only' => false, 'dependencies' => array(), '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 */ function prepare_asset_url(string $dir) { // Get the path to wp-content $content_dir = wp_normalize_path(plugin_dir_path(__FILE__)); // Using plugin_dir_path instead of 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. * * @return array */ function register_asset(string $manifest_dir, string $entry, array $options): ?array { try { $manifest = get_manifest($manifest_dir); } catch (Exception $e) { if (defined('WP_DEBUG') && WP_DEBUG) { wp_die(esc_html($e->getMessage())); } return null; } $options = parse_options($options); $assets = $manifest->is_dev ? load_development_asset($manifest, $entry, $options) : 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. * * @return bool */ function enqueue_asset(string $manifest_dir, string $entry, array $options): bool { $assets = register_asset($manifest_dir, $entry, $options); if (is_null($assets)) { return false; } $map = array( 'scripts' => 'wp_enqueue_script', 'styles' => 'wp_enqueue_style', ); foreach ($assets as $group => $handles) { $func = $map[$group]; foreach ($handles as $handle) { $func($handle); } } return true; }