PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.0.6
WindPress – Tailwind CSS integration for WordPress v3.0.6
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.6, at src/Utils/AssetVite.php

449 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 WIND_PRESS;
15 use Exception;
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 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 * @return bool
80 */
81 public function enqueue_asset(string $entry, array $options) : bool
82 {
83 return $this->_enqueue_asset(\dirname(WIND_PRESS::FILE) . '/build', $entry, $options);
84 }
85 /**
86 * Register asset
87 *
88 * @since 0.1.0
89 *
90 * @param string $entry Entrypoint to enqueue.
91 * @param array $options Enqueue options:
92 * - 'handle' (string) The handle for the enqueued asset.
93 * - 'dependencies' (array) An array of handles for assets this asset depends on.
94 * - 'in-footer' (bool) Whether to enqueue the asset in the footer.
95 * - 'css-dependencies' (array) An array of handles for CSS assets this CSS asset depends on.
96 * - 'css-media' (string) The media attribute value for the stylesheet tag.
97 * - 'css-only' (bool) Whether this asset is only CSS.
98 * @return array|null
99 */
100 public function register_asset(string $entry, array $options) : ?array
101 {
102 return $this->_register_asset(\dirname(WIND_PRESS::FILE) . '/build', $entry, $options);
103 }
104 /**
105 * Get the asset base absolute path.
106 *
107 * @return string The asset base absolute path.
108 */
109 public static function asset_base_url() : string
110 {
111 return \plugins_url('build/', WIND_PRESS::FILE);
112 }
113 /**
114 * Get manifest data
115 *
116 * @since 0.1.0
117 * @since 0.8.0 Use wp_json_file_decode().
118 *
119 * @param string $manifest_dir Path to manifest directory.
120 *
121 * @throws Exception Exception is thrown when the file doesn't exist, unreadble, or contains invalid data.
122 *
123 * @return object Object containing manifest type and data.
124 */
125 public function get_manifest(string $manifest_dir) : object
126 {
127 $dev_manifest = 'vite-dev-server';
128 // Avoid repeatedly opening & decoding the same file.
129 static $manifests = [];
130 $file_names = [$dev_manifest, 'manifest'];
131 foreach ($file_names as $file_name) {
132 $is_dev = $file_name === $dev_manifest;
133 $manifest_path = "{$manifest_dir}/{$file_name}.json";
134 if (isset($manifests[$manifest_path])) {
135 return $manifests[$manifest_path];
136 }
137 if (\is_file($manifest_path) && \is_readable($manifest_path)) {
138 break;
139 }
140 unset($manifest_path);
141 }
142 if (!isset($manifest_path)) {
143 throw new Exception(\esc_html(\sprintf('[Vite] No manifest found in %s.', $manifest_dir)));
144 }
145 $manifest = \wp_json_file_decode($manifest_path);
146 if (!$manifest) {
147 throw new Exception(\esc_html(\sprintf('[Vite] Failed to read manifest file %s.', $manifest_path)));
148 }
149 /**
150 * Filter manifest data
151 *
152 * @param array $manifest Manifest data.
153 * @param string $manifest_dir Manifest directory path.
154 * @param string $manifest_path Manifest file path.
155 * @param bool $is_dev Whether this is a manifest for development assets.
156 */
157 $manifest = \apply_filters('f!windpress/utils/asset_vite/vite_for_wp__manifest_data', $manifest, $manifest_dir, $manifest_path);
158 $manifests[$manifest_path] = (object) ['data' => $manifest, 'dir' => $manifest_dir, 'is_dev' => $is_dev];
159 return $manifests[$manifest_path];
160 }
161 /**
162 * Filter script tag
163 *
164 * This creates a function to be used as callback for the `script_loader` filter
165 * which adds `type="module"` attribute to the script tag.
166 *
167 * @since 0.1.0
168 *
169 * @param string $handle Script handle.
170 *
171 * @return void
172 */
173 public function filter_script_tag(string $handle) : void
174 {
175 \add_filter('script_loader_tag', fn(...$args) => $this->set_script_type_attribute($handle, ...$args), 10, 3);
176 }
177 /**
178 * Add `type="module"` to a script tag
179 *
180 * @since 0.1.0
181 * @since 0.8.0 Use WP_HTML_Tag_Processor.
182 *
183 * @param string $target_handle Handle of the script being targeted by the filter callback.
184 * @param string $tag Original script tag.
185 * @param string $handle Handle of the script that's currently being filtered.
186 * @param string $src Script source.
187 *
188 * @return string Script tag with attribute `type="module"` added.
189 */
190 public function set_script_type_attribute(string $target_handle, string $tag, string $handle, string $src) : string
191 {
192 if ($target_handle !== $handle) {
193 return $tag;
194 }
195 $processor = new WP_HTML_Tag_Processor($tag);
196 $script_found = \false;
197 do {
198 $script_found = $processor->next_tag('script');
199 } while ($processor->get_attribute('src') !== $src);
200 if ($script_found) {
201 $processor->set_attribute('type', 'module');
202 }
203 return $processor->get_updated_html();
204 }
205 /**
206 * Generate development asset src
207 *
208 * @since 0.1.0
209 *
210 * @param object $manifest Asset manifest.
211 * @param string $entry Asset entry name.
212 *
213 * @return string
214 */
215 public function generate_development_asset_src(object $manifest, string $entry) : string
216 {
217 return \sprintf('%s/%s', \untrailingslashit($manifest->data->origin), \trim(\preg_replace('/[\\/]{2,}/', '/', "{$manifest->data->base}/{$entry}"), '/'));
218 }
219 /**
220 * Register vite client script
221 *
222 * @since 0.1.0
223 *
224 * @param object $manifest Asset manifest.
225 *
226 * @return void
227 */
228 public function register_vite_client_script(object $manifest) : void
229 {
230 if (\wp_script_is(self::VITE_CLIENT_SCRIPT_HANDLE)) {
231 return;
232 }
233 $src = $this->generate_development_asset_src($manifest, '@vite/client');
234 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
235 \wp_register_script(self::VITE_CLIENT_SCRIPT_HANDLE, $src, [], null, \false);
236 $this->filter_script_tag(self::VITE_CLIENT_SCRIPT_HANDLE);
237 }
238 /**
239 * Inject react-refresh preamble script once, if needed
240 *
241 * @since 0.8.0
242 *
243 * @param object $manifest Asset manifest.
244 * @return void
245 */
246 public function inject_react_refresh_preamble_script(object $manifest) : void
247 {
248 static $is_react_refresh_preamble_printed = \false;
249 if ($is_react_refresh_preamble_printed) {
250 return;
251 }
252 if (!\in_array('vite:react-refresh', $manifest->data->plugins, \true)) {
253 return;
254 }
255 $react_refresh_script_src = $this->generate_development_asset_src($manifest, '@react-refresh');
256 $script_position = 'after';
257 $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 ";
258 // escape the script to prevent it from being executed by the browser
259 \wp_add_inline_script(self::VITE_CLIENT_SCRIPT_HANDLE, $script, $script_position);
260 \add_filter('wp_inline_script_attributes', function (array $attributes) use($script_position) : array {
261 if (isset($attributes['id']) && $attributes['id'] === self::VITE_CLIENT_SCRIPT_HANDLE . "-js-{$script_position}") {
262 $attributes['type'] = 'module';
263 }
264 return $attributes;
265 });
266 $is_react_refresh_preamble_printed = \true;
267 }
268 /**
269 * Load development asset
270 *
271 * @since 0.1.0
272 *
273 * @param object $manifest Asset manifest.
274 * @param string $entry Entrypoint to enqueue.
275 * @param array $options Enqueue options.
276 *
277 * @return array|null Array containing registered scripts or NULL if the none was registered.
278 */
279 public function load_development_asset(object $manifest, string $entry, array $options) : ?array
280 {
281 $this->register_vite_client_script($manifest);
282 $this->inject_react_refresh_preamble_script($manifest);
283 $dependencies = \array_merge([self::VITE_CLIENT_SCRIPT_HANDLE], $options['dependencies']);
284 $src = $this->generate_development_asset_src($manifest, $entry);
285 $this->filter_script_tag($options['handle']);
286 // This is a development script, browsers shouldn't cache it.
287 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
288 if (!\wp_register_script($options['handle'], $src, $dependencies, null, $options['in-footer'])) {
289 return null;
290 }
291 $assets = ['scripts' => [$options['handle']], 'styles' => $options['css-dependencies']];
292 /**
293 * Filter registered development assets
294 *
295 * @param array $assets Registered assets.
296 * @param object $manifest Manifest object.
297 * @param string $entry Entrypoint file.
298 * @param array $options Enqueue options.
299 */
300 $assets = \apply_filters('f!windpress/utils/asset_vite/vite_for_wp__development_assets', $assets, $manifest, $entry, $options);
301 return $assets;
302 }
303 /**
304 * Load production asset
305 *
306 * @since 0.1.0
307 *
308 * @param object $manifest Asset manifest.
309 * @param string $entry Entrypoint to enqueue.
310 * @param array $options Enqueue options.
311 *
312 * @return array|null Array containing registered scripts & styles or NULL if there was an error.
313 */
314 public function load_production_asset(object $manifest, string $entry, array $options) : ?array
315 {
316 $url = $this->prepare_asset_url($manifest->dir);
317 if (!isset($manifest->data->{$entry})) {
318 if (\defined('WP_DEBUG') && \WP_DEBUG) {
319 \wp_die(\esc_html(\sprintf('[Vite] Entry %s not found.', $entry)));
320 }
321 return null;
322 }
323 $assets = ['scripts' => [], 'styles' => []];
324 $item = $manifest->data->{$entry};
325 $src = "{$url}/{$item->file}";
326 if (!$options['css-only']) {
327 $this->filter_script_tag($options['handle']);
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_script($options['handle'], $src, $options['dependencies'], null, $options['in-footer'])) {
331 $assets['scripts'][] = $options['handle'];
332 }
333 }
334 if (!empty($item->css)) {
335 foreach ($item->css as $index => $css_file_path) {
336 $style_handle = "{$options['handle']}-{$index}";
337 // Don't worry about browser caching as the version is embedded in the file name.
338 // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
339 if (\wp_register_style($style_handle, "{$url}/{$css_file_path}", $options['css-dependencies'], null, $options['css-media'])) {
340 $assets['styles'][] = $style_handle;
341 }
342 }
343 }
344 /**
345 * Filter registered production assets
346 *
347 * @param array $assets Registered assets.
348 * @param object $manifest Manifest object.
349 * @param string $entry Entrypoint file.
350 * @param array $options Enqueue options.
351 */
352 $assets = \apply_filters('f!windpress/utils/asset_vite/vite_for_wp__production_assets', $assets, $manifest, $entry, $options);
353 return $assets;
354 }
355 /**
356 * Parse register/enqueue options
357 *
358 * @since 0.1.0
359 *
360 * @param array $options Array of options.
361 *
362 * @return array Array of options merged with defaults.
363 */
364 public function parse_options(array $options) : array
365 {
366 $defaults = ['css-dependencies' => [], 'css-media' => 'all', 'css-only' => \false, 'dependencies' => [], 'handle' => '', 'in-footer' => \false];
367 return \wp_parse_args($options, $defaults);
368 }
369 /**
370 * Prepare asset url
371 *
372 * @author Justin Slamka <jslamka5685@gmail.com>
373 * @since 0.4.0
374 * @since 0.6.1 Normalize paths so they work on Windows as well.
375 *
376 * @param string $dir Asset directory.
377 *
378 * @return string
379 */
380 public function prepare_asset_url(string $dir)
381 {
382 $content_dir = \wp_normalize_path(\WP_CONTENT_DIR);
383 $manifest_dir = \wp_normalize_path($dir);
384 $url = \content_url(\str_replace($content_dir, '', $manifest_dir));
385 $url_matches_pattern = \preg_match('/(?<address>http(?:s?):\\/\\/.*\\/)(?<fullPath>wp-content(?<removablePath>\\/.*)\\/(?:plugins|themes)\\/.*)/', $url, $url_parts);
386 if ($url_matches_pattern === 0) {
387 return $url;
388 }
389 ['address' => $address, 'fullPath' => $full_path, 'removablePath' => $removable_path] = $url_parts;
390 return \sprintf('%s%s', $address, \str_replace($removable_path, '', $full_path));
391 }
392 /**
393 * Register asset
394 *
395 * @since 0.1.0
396 *
397 * @see load_development_asset
398 * @see load_production_asset
399 *
400 * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`.
401 * @param string $entry Entrypoint to enqueue.
402 * @param array $options Enqueue options.
403 *
404 * @return array
405 */
406 public function _register_asset(string $manifest_dir, string $entry, array $options) : ?array
407 {
408 try {
409 $manifest = $this->get_manifest($manifest_dir);
410 } catch (Exception $e) {
411 if (\defined('WP_DEBUG') && \WP_DEBUG) {
412 \wp_die(\esc_html($e->getMessage()));
413 }
414 return null;
415 }
416 $options = $this->parse_options($options);
417 $assets = $manifest->is_dev ? $this->load_development_asset($manifest, $entry, $options) : $this->load_production_asset($manifest, $entry, $options);
418 return $assets;
419 }
420 /**
421 * Enqueue asset
422 *
423 * @since 0.1.0
424 *
425 * @see _register_asset
426 *
427 * @param string $manifest_dir Path to directory containing manifest file, usually `build` or `dist`.
428 * @param string $entry Entrypoint to enqueue.
429 * @param array $options Enqueue options.
430 *
431 * @return bool
432 */
433 public function _enqueue_asset(string $manifest_dir, string $entry, array $options) : bool
434 {
435 $assets = $this->_register_asset($manifest_dir, $entry, $options);
436 if (\is_null($assets)) {
437 return \false;
438 }
439 $map = ['scripts' => 'wp_enqueue_script', 'styles' => 'wp_enqueue_style'];
440 foreach ($assets as $group => $handles) {
441 $func = $map[$group];
442 foreach ($handles as $handle) {
443 $func($handle);
444 }
445 }
446 return \true;
447 }
448 }
449