PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.0.16
WindPress – Tailwind CSS integration for WordPress v3.0.16
3.2.89 3.2.88 3.2.87 3.2.86 3.2.85 3.2.84 3.2.83 3.2.82 3.2.81 trunk 3.0.0 3.0.1 3.0.10 3.0.11 3.0.12 3.0.13 3.0.14 3.0.15 3.0.16 3.0.17 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 All 143 releases
windpress / src / Utils / AssetVite.php

AssetVite.php in WindPress – Tailwind CSS integration for WordPress 3.0.16, at src/Utils/AssetVite.php

436 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the WindPress package.
5 *
6 * (c) Joshua Gugun Siagian <suabahasa@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11 declare (strict_types=1);
12 namespace WindPress\WindPress\Utils;
13
14 use Exception;
15 use WIND_PRESS;
16 use WP_HTML_Tag_Processor;
17 /**
18 * Manifest friendly assets manager.
19 *
20 * @todo Add translation support. Use filter hook
21 *
22 * @since 3.0.0
23 */
24 class AssetVite
25 {
26 public const VITE_CLIENT_SCRIPT_HANDLE = 'vite-client';
27 /**
28 * Stores the instance, implementing a Singleton pattern.
29 */
30 private static self $instance;
31 /**
32 * The Singleton's constructor should always be private to prevent direct
33 * construction calls with the `new` operator.
34 */
35 private function __construct()
36 {
37 }
38 /**
39 * Singletons should not be cloneable.
40 */
41 private function __clone()
42 {
43 }
44 /**
45 * Singletons should not be restorable from strings.
46 *
47 * @throws Exception Cannot unserialize a singleton.
48 */
49 public function __wakeup()
50 {
51 throw new Exception('Cannot unserialize a singleton.');
52 }
53 /**
54 * This is the static method that controls the access to the singleton
55 * instance. On the first run, it creates a singleton object and places it
56 * into the static property. On subsequent runs, it returns the client existing
57 * object stored in the static property.
58 */
59 public static function get_instance() : self
60 {
61 if (!isset(self::$instance)) {
62 self::$instance = new self();
63 }
64 return self::$instance;
65 }
66 /**
67 * Enqueue asset
68 *
69 * @since 0.1.0
70 *
71 * @param string $entry Entrypoint to enqueue.
72 * @param array $options Enqueue options:
73 * - 'handle' (string) The handle for the enqueued asset.
74 * - 'dependencies' (array) An array of handles for assets this asset depends on.
75 * - 'in-footer' (bool) Whether to enqueue the asset in the footer.
76 * - 'css-dependencies' (array) An array of handles for CSS assets this CSS asset depends on.
77 * - 'css-media' (string) The media attribute value for the stylesheet tag.
78 * - 'css-only' (bool) Whether this asset is only CSS.
79 */
80 public function enqueue_asset(string $entry, array $options) : bool
81 {
82 return $this->_enqueue_asset(\dirname(WIND_PRESS::FILE) . '/build', $entry, $options);
83 }
84 /**
85 * Register asset
86 *
87 * @since 0.1.0
88 *
89 * @param string $entry Entrypoint to enqueue.
90 * @param array $options Enqueue options:
91 * - 'handle' (string) The handle for the enqueued asset.
92 * - 'dependencies' (array) An array of handles for assets this asset depends on.
93 * - 'in-footer' (bool) Whether to enqueue the asset in the footer.
94 * - 'css-dependencies' (array) An array of handles for CSS assets this CSS asset depends on.
95 * - 'css-media' (string) The media attribute value for the stylesheet tag.
96 * - 'css-only' (bool) Whether this asset is only CSS.
97 */
98 public function register_asset(string $entry, array $options) : ?array
99 {
100 return $this->_register_asset(\dirname(WIND_PRESS::FILE) . '/build', $entry, $options);
101 }
102 /**
103 * Get the asset base absolute path.
104 *
105 * @return string The asset base absolute path.
106 */
107 public static function asset_base_url() : string
108 {
109 return \plugins_url('build/', WIND_PRESS::FILE);
110 }
111 /**
112 * Get manifest data
113 *
114 * @since 0.1.0
115 * @since 0.8.0 Use wp_json_file_decode().
116 *
117 * @param string $manifest_dir Path to manifest directory.
118 *
119 * @throws Exception Exception is thrown when the file doesn't exist, unreadble, or contains invalid data.
120 *
121 * @return object Object containing manifest type and data.
122 */
123 public function get_manifest(string $manifest_dir) : object
124 {
125 $dev_manifest = 'vite-dev-server';
126 // Avoid repeatedly opening & decoding the same file.
127 static $manifests = [];
128 $file_names = [$dev_manifest, 'manifest'];
129 foreach ($file_names as $file_name) {
130 $is_dev = $file_name === $dev_manifest;
131 $manifest_path = \sprintf('%s/%s.json', $manifest_dir, $file_name);
132 if (isset($manifests[$manifest_path])) {
133 return $manifests[$manifest_path];
134 }
135 if (\is_file($manifest_path) && \is_readable($manifest_path)) {
136 break;
137 }
138 unset($manifest_path);
139 }
140 if (!isset($manifest_path)) {
141 throw new Exception(\esc_html(\sprintf('[Vite] No manifest found in %s.', $manifest_dir)));
142 }
143 $manifest = \wp_json_file_decode($manifest_path);
144 if (!$manifest) {
145 throw new Exception(\esc_html(\sprintf('[Vite] Failed to read manifest file %s.', $manifest_path)));
146 }
147 /**
148 * Filter manifest data
149 *
150 * @param array $handle Manifest data.
151 * @param string $manifest_dir Manifest directory path.
152 * @param string $manifest_path Manifest file path.
153 * @param bool $is_dev Whether this is a manifest for development assets.
154 */
155 $manifest = \apply_filters('f!windpress/utils/asset_vite/vite_for_wp__manifest_data', $manifest, $manifest_dir, $manifest_path);
156 $manifests[$manifest_path] = (object) ['data' => $manifest, 'dir' => $manifest_dir, 'is_dev' => $is_dev];
157 return $manifests[$manifest_path];
158 }
159 /**
160 * Filter script tag
161 *
162 * This creates a function to be used as callback for the `script_loader` filter
163 * which adds `type="module"` attribute to the script tag.
164 *
165 * @since 0.1.0
166 *
167 * @param string $handle Script handle.
168 */
169 public function filter_script_tag(string $handle) : void
170 {
171 \add_filter('script_loader_tag', fn(...$args) => $this->set_script_type_attribute($handle, ...$args), 10, 3);
172 }
173 /**
174 * Add `type="module"` to a script tag
175 *
176 * @since 0.1.0
177 * @since 0.8.0 Use WP_HTML_Tag_Processor.
178 *
179 * @param string $target_handle Handle of the script being targeted by the filter callback.
180 * @param string $tag Original script tag.
181 * @param string $handle Handle of the script that's currently being filtered.
182 * @param string $src Script source.
183 *
184 * @return string Script tag with attribute `type="module"` added.
185 */
186 public function set_script_type_attribute(string $target_handle, string $tag, string $handle, string $src) : string
187 {
188 if ($target_handle !== $handle) {
189 return $tag;
190 }
191 $wphtmlTagProcessor = new WP_HTML_Tag_Processor($tag);
192 $script_found = \false;
193 do {
194 $script_found = $wphtmlTagProcessor->next_tag('script');
195 } while ($wphtmlTagProcessor->get_attribute('src') !== $src);
196 if ($script_found) {
197 $wphtmlTagProcessor->set_attribute('type', 'module');
198 }
199 return $wphtmlTagProcessor->get_updated_html();
200 }
201 /**
202 * Generate development asset src
203 *
204 * @since 0.1.0
205 *
206 * @param object $manifest Asset manifest.
207 * @param string $entry Asset entry name.
208 */
209 public function generate_development_asset_src(object $manifest, string $entry) : string
210 {
211 return \sprintf('%s/%s', \untrailingslashit($manifest->data->origin), \trim(\preg_replace('/[\\/]{2,}/', '/', \sprintf('%s/%s', $manifest->data->base, $entry)), '/'));
212 }
213 /**
214 * Register vite client script
215 *
216 * @since 0.1.0
217 *
218 * @param object $manifest Asset manifest.
219 */
220 public function register_vite_client_script(object $manifest) : void
221 {
222 if (\wp_script_is(self::VITE_CLIENT_SCRIPT_HANDLE)) {
223 return;
224 }
225 $src = $this->generate_development_asset_src($manifest, '@vite/client');
226 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
227 \wp_register_script(self::VITE_CLIENT_SCRIPT_HANDLE, $src, [], null, \false);
228 $this->filter_script_tag(self::VITE_CLIENT_SCRIPT_HANDLE);
229 }
230 /**
231 * Inject react-refresh preamble script once, if needed
232 *
233 * @since 0.8.0
234 *
235 * @param object $manifest Asset manifest.
236 */
237 public function inject_react_refresh_preamble_script(object $manifest) : void
238 {
239 static $is_react_refresh_preamble_printed = \false;
240 if ($is_react_refresh_preamble_printed) {
241 return;
242 }
243 if (!\in_array('vite:react-refresh', $manifest->data->plugins, \true)) {
244 return;
245 }
246 $react_refresh_script_src = $this->generate_development_asset_src($manifest, '@react-refresh');
247 $script_position = 'after';
248 $script = "\n import RefreshRuntime from \"{$react_refresh_script_src}\";\n RefreshRuntime.injectIntoGlobalHook(window);\n window.\$RefreshReg\$ = () => {};\n window.\$RefreshSig\$ = () => (type) => type;\n window.__vite_plugin_react_preamble_installed__ = true;\n ";
249 // escape the script to prevent it from being executed by the browser
250 \wp_add_inline_script(self::VITE_CLIENT_SCRIPT_HANDLE, $script, $script_position);
251 \add_filter('wp_inline_script_attributes', function (array $attributes) use($script_position) : array {
252 if (isset($attributes['id']) && $attributes['id'] === self::VITE_CLIENT_SCRIPT_HANDLE . ('-js-' . $script_position)) {
253 $attributes['type'] = 'module';
254 }
255 return $attributes;
256 });
257 $is_react_refresh_preamble_printed = \true;
258 }
259 /**
260 * Load development asset
261 *
262 * @since 0.1.0
263 *
264 * @param object $manifest Asset manifest.
265 * @param string $entry Entrypoint to enqueue.
266 * @param array $options Enqueue options.
267 *
268 * @return array|null Array containing registered scripts or NULL if the none was registered.
269 */
270 public function load_development_asset(object $manifest, string $entry, array $options) : ?array
271 {
272 $this->register_vite_client_script($manifest);
273 $this->inject_react_refresh_preamble_script($manifest);
274 $dependencies = \array_merge([self::VITE_CLIENT_SCRIPT_HANDLE], $options['dependencies']);
275 $src = $this->generate_development_asset_src($manifest, $entry);
276 $this->filter_script_tag($options['handle']);
277 // This is a development script, browsers shouldn't cache it.
278 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
279 if (!\wp_register_script($options['handle'], $src, $dependencies, null, $options['in-footer'])) {
280 return null;
281 }
282 $assets = ['scripts' => [$options['handle']], 'styles' => $options['css-dependencies']];
283 /**
284 * Filter registered development assets
285 *
286 * @param array $assets Registered assets.
287 * @param object $manifest Manifest object.
288 * @param string $entry Entrypoint file.
289 * @param array $options Enqueue options.
290 */
291 $assets = \apply_filters('f!windpress/utils/asset_vite/vite_for_wp__development_assets', $assets, $manifest, $entry, $options);
292 return $assets;
293 }
294 /**
295 * Load production asset
296 *
297 * @since 0.1.0
298 *
299 * @param object $manifest Asset manifest.
300 * @param string $entry Entrypoint to enqueue.
301 * @param array $options Enqueue options.
302 *
303 * @return array|null Array containing registered scripts & styles or NULL if there was an error.
304 */
305 public function load_production_asset(object $manifest, string $entry, array $options) : ?array
306 {
307 $url = $this->prepare_asset_url($manifest->dir);
308 if (!isset($manifest->data->{$entry})) {
309 if (\defined('WP_DEBUG') && \WP_DEBUG) {
310 \wp_die(\esc_html(\sprintf('[Vite] Entry %s not found.', $entry)));
311 }
312 return null;
313 }
314 $assets = ['scripts' => [], 'styles' => []];
315 $item = $manifest->data->{$entry};
316 $src = \sprintf('%s/%s', $url, $item->file);
317 if (!$options['css-only']) {
318 $this->filter_script_tag($options['handle']);
319 // Don't worry about browser caching as the version is embedded in the file name.
320 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
321 if (\wp_register_script($options['handle'], $src, $options['dependencies'], null, $options['in-footer'])) {
322 $assets['scripts'][] = $options['handle'];
323 }
324 }
325 if (!empty($item->css)) {
326 foreach ($item->css as $index => $css_file_path) {
327 $style_handle = \sprintf('%s-%s', $options['handle'], $index);
328 // Don't worry about browser caching as the version is embedded in the file name.
329 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
330 if (\wp_register_style($style_handle, \sprintf('%s/%s', $url, $css_file_path), $options['css-dependencies'], null, $options['css-media'])) {
331 $assets['styles'][] = $style_handle;
332 }
333 }
334 }
335 /**
336 * Filter registered production assets
337 *
338 * @param array $assets Registered assets.
339 * @param object $manifest Manifest object.
340 * @param string $entry Entrypoint file.
341 * @param array $options Enqueue options.
342 */
343 $assets = \apply_filters('f!windpress/utils/asset_vite/vite_for_wp__production_assets', $assets, $manifest, $entry, $options);
344 return $assets;
345 }
346 /**
347 * Parse register/enqueue options
348 *
349 * @since 0.1.0
350 *
351 * @param array $options Array of options.
352 *
353 * @return array Array of options merged with defaults.
354 */
355 public function parse_options(array $options) : array
356 {
357 $defaults = ['css-dependencies' => [], 'css-media' => 'all', 'css-only' => \false, 'dependencies' => [], 'handle' => '', 'in-footer' => \false];
358 return \wp_parse_args($options, $defaults);
359 }
360 /**
361 * Prepare asset url
362 *
363 * @author Justin Slamka <jslamka5685@gmail.com>
364 * @since 0.4.0
365 * @since 0.6.1 Normalize paths so they work on Windows as well.
366 *
367 * @param string $dir Asset directory.
368 *
369 * @return string
370 */
371 public function prepare_asset_url(string $dir)
372 {
373 $content_dir = \wp_normalize_path(\WP_CONTENT_DIR);
374 $manifest_dir = \wp_normalize_path($dir);
375 $url = \content_url(\str_replace($content_dir, '', $manifest_dir));
376 $url_matches_pattern = \preg_match('/(?<address>http(?:s?):\\/\\/.*\\/)(?<fullPath>wp-content(?<removablePath>\\/.*)\\/(?:plugins|themes)\\/.*)/', $url, $url_parts);
377 if ($url_matches_pattern === 0) {
378 return $url;
379 }
380 ['address' => $address, 'fullPath' => $full_path, 'removablePath' => $removable_path] = $url_parts;
381 return \sprintf('%s%s', $address, \str_replace($removable_path, '', $full_path));
382 }
383 /**
384 * Register asset
385 *
386 * @since 0.1.0
387 *
388 * @see load_development_asset
389 * @see load_production_asset
390 *
391 * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`.
392 * @param string $entry Entrypoint to enqueue.
393 * @param array $options Enqueue options.
394 */
395 public function _register_asset(string $manifest_dir, string $entry, array $options) : ?array
396 {
397 try {
398 $manifest = $this->get_manifest($manifest_dir);
399 } catch (Exception $exception) {
400 if (\defined('WP_DEBUG') && \WP_DEBUG) {
401 \wp_die(\esc_html($exception->getMessage()));
402 }
403 return null;
404 }
405 $options = $this->parse_options($options);
406 $assets = $manifest->is_dev ? $this->load_development_asset($manifest, $entry, $options) : $this->load_production_asset($manifest, $entry, $options);
407 return $assets;
408 }
409 /**
410 * Enqueue asset
411 *
412 * @since 0.1.0
413 *
414 * @see _register_asset
415 *
416 * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`.
417 * @param string $entry Entrypoint to enqueue.
418 * @param array $options Enqueue options.
419 */
420 public function _enqueue_asset(string $manifest_dir, string $entry, array $options) : bool
421 {
422 $assets = $this->_register_asset($manifest_dir, $entry, $options);
423 if ($assets === null) {
424 return \false;
425 }
426 $map = ['scripts' => 'wp_enqueue_script', 'styles' => 'wp_enqueue_style'];
427 foreach ($assets as $group => $handles) {
428 $func = $map[$group];
429 foreach ($handles as $handle) {
430 $func($handle);
431 }
432 }
433 return \true;
434 }
435 }
436