PluginProbe
WindPress – Tailwind CSS integration for WordPress / 3.2.88
WindPress – Tailwind CSS integration for WordPress v3.2.88
3.2.90 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 All 144 releases
windpress / vendor / nabasa / vp-wp / vp-wp.php

vp-wp.php in WindPress – Tailwind CSS integration for WordPress 3.2.88, at vendor/nabasa/vp-wp/vp-wp.php

466 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare (strict_types=1);
4 namespace WindPressDeps\Nabasa\VitePlus;
5
6 use Exception;
7 use WP_HTML_Tag_Processor;
8 const DEV_MANIFEST_FILE = 'vite-dev-server.json';
9 const VITE_CLIENT_HANDLE = 'vp-wp-client';
10 /**
11 * Reusable asset helper bound to a single manifest directory.
12 */
13 final class Assets
14 {
15 private string $manifest_dir;
16 private string $scope;
17 /**
18 * @param string $manifest_dir Absolute path to the directory that contains `manifest.json`
19 * and `vite-dev-server.json`.
20 * @param string $scope Optional hook scope. Scoped hooks are normalized with `sanitize_key()`.
21 */
22 public function __construct(string $manifest_dir, string $scope = '')
23 {
24 $this->manifest_dir = $manifest_dir;
25 $this->scope = normalize_scope($scope);
26 }
27 /**
28 * @return string Absolute path to the bound manifest directory.
29 */
30 public function manifest_dir(): string
31 {
32 return $this->manifest_dir;
33 }
34 /**
35 * @return string Normalized hook scope for this asset helper.
36 */
37 public function scope(): string
38 {
39 return $this->scope;
40 }
41 /**
42 * Register a Vite entry and its extracted CSS files.
43 *
44 * @param string $entry Manifest entry key, such as `resources/app.ts`.
45 * @param array{
46 * handle?: string,
47 * dependencies?: list<string>,
48 * css_dependencies?: list<string>,
49 * css_media?: string,
50 * css_only?: bool,
51 * in_footer?: bool
52 * } $options Asset registration options.
53 * @return array{
54 * scripts: list<string>,
55 * styles: list<string>
56 * }|null Registered WordPress handles or `null` when registration fails.
57 */
58 public function register(string $entry, array $options = []): ?array
59 {
60 return register_asset($this->manifest_dir, $entry, $options, $this->scope);
61 }
62 /**
63 * Register and immediately enqueue a Vite entry.
64 *
65 * @param string $entry Manifest entry key, such as `resources/app.ts`.
66 * @param array{
67 * handle?: string,
68 * dependencies?: list<string>,
69 * css_dependencies?: list<string>,
70 * css_media?: string,
71 * css_only?: bool,
72 * in_footer?: bool
73 * } $options Asset enqueue options.
74 * @return bool True when at least the registration flow succeeds, otherwise false.
75 */
76 public function enqueue(string $entry, array $options = []): bool
77 {
78 return enqueue_asset($this->manifest_dir, $entry, $options, $this->scope);
79 }
80 /**
81 * Resolve the public URL for a file inside the bound manifest directory.
82 *
83 * @param string $asset Relative asset path inside the manifest directory.
84 * @return string Public asset URL.
85 */
86 public function url(string $asset = ''): string
87 {
88 return asset_url($this->manifest_dir, $asset);
89 }
90 }
91 /**
92 * Create a reusable asset helper for a manifest directory.
93 *
94 * @param string $manifest_dir Absolute path to the directory that contains `manifest.json`
95 * and `vite-dev-server.json`.
96 * @param string $scope Optional hook scope. Scoped hooks are normalized with `sanitize_key()`.
97 */
98 function assets(string $manifest_dir, string $scope = ''): Assets
99 {
100 return new Assets($manifest_dir, $scope);
101 }
102 /**
103 * Register and enqueue a Vite entry from a manifest directory.
104 *
105 * @param string $manifest_dir Absolute path to the directory that contains `manifest.json`
106 * and `vite-dev-server.json`.
107 * @param string $entry Manifest entry key, such as `resources/app.ts`.
108 * @param array{
109 * handle?: string,
110 * dependencies?: list<string>,
111 * css_dependencies?: list<string>,
112 * css_media?: string,
113 * css_only?: bool,
114 * in_footer?: bool
115 * } $options Asset enqueue options.
116 * @param string $scope Optional hook scope. Scoped hooks are normalized with `sanitize_key()`.
117 * @return bool True when the asset flow succeeds, otherwise false.
118 */
119 function enqueue_asset(string $manifest_dir, string $entry, array $options = [], string $scope = ''): bool
120 {
121 $assets = register_asset($manifest_dir, $entry, $options, $scope);
122 if (null === $assets) {
123 return \false;
124 }
125 $callbacks = ['scripts' => 'wp_enqueue_script', 'styles' => 'wp_enqueue_style'];
126 foreach ($assets as $group => $handles) {
127 $callback = $callbacks[$group];
128 foreach ($handles as $handle) {
129 $callback($handle);
130 }
131 }
132 return \true;
133 }
134 /**
135 * Register a Vite entry and its related assets from a manifest directory.
136 *
137 * @param string $manifest_dir Absolute path to the directory that contains `manifest.json`
138 * and `vite-dev-server.json`.
139 * @param string $entry Manifest entry key, such as `resources/app.ts`.
140 * @param array{
141 * handle?: string,
142 * dependencies?: list<string>,
143 * css_dependencies?: list<string>,
144 * css_media?: string,
145 * css_only?: bool,
146 * in_footer?: bool
147 * } $options Asset registration options.
148 * @param string $scope Optional hook scope. Scoped hooks are normalized with `sanitize_key()`.
149 * @return array{
150 * scripts: list<string>,
151 * styles: list<string>
152 * }|null Registered WordPress handles or `null` when registration fails.
153 */
154 function register_asset(string $manifest_dir, string $entry, array $options = [], string $scope = ''): ?array
155 {
156 $scope = normalize_scope($scope);
157 try {
158 $manifest = get_manifest($manifest_dir, $scope);
159 } catch (Exception $exception) {
160 if (defined('WP_DEBUG') && \WP_DEBUG) {
161 wp_die(esc_html($exception->getMessage()));
162 }
163 return null;
164 }
165 $options = parse_options($options);
166 if ('' === $options['handle']) {
167 $options['handle'] = default_asset_handle($entry);
168 }
169 return $manifest->is_dev ? load_development_asset($manifest, $entry, $options, $scope) : load_production_asset($manifest, $entry, $options, $scope);
170 }
171 /**
172 * Load and cache the first available manifest for a directory.
173 *
174 * Prefers the Vite development manifest when present, then falls back to the
175 * production `manifest.json` file.
176 *
177 * @param string $manifest_dir Absolute path to the directory that contains manifest files.
178 * @param string $scope Optional hook scope. Scoped hooks are normalized with `sanitize_key()`.
179 * @return object{
180 * data: object,
181 * dir: string,
182 * is_dev: bool
183 * } Decoded manifest data with directory metadata.
184 * @throws Exception When no readable manifest exists or the manifest cannot be decoded.
185 */
186 function get_manifest(string $manifest_dir, string $scope = ''): object
187 {
188 static $manifests = [];
189 $manifest_paths = ["{$manifest_dir}/" . DEV_MANIFEST_FILE, "{$manifest_dir}/manifest.json"];
190 foreach ($manifest_paths as $manifest_path) {
191 if (isset($manifests[$manifest_path])) {
192 return $manifests[$manifest_path];
193 }
194 if (is_file($manifest_path) && is_readable($manifest_path)) {
195 $is_dev = string_ends_with($manifest_path, DEV_MANIFEST_FILE);
196 break;
197 }
198 }
199 if (!isset($manifest_path, $is_dev)) {
200 throw new Exception(sprintf('[vp-wp] No manifest found in %s.', $manifest_dir));
201 }
202 $manifest = wp_json_file_decode($manifest_path, ['associative' => \false]);
203 if (!$manifest) {
204 throw new Exception(sprintf('[vp-wp] Failed to read manifest file %s.', $manifest_path));
205 }
206 $manifest = filter_value('manifest_data', $manifest, $scope, $manifest_dir, $manifest_path, $is_dev);
207 $manifests[$manifest_path] = (object) ['data' => $manifest, 'dir' => $manifest_dir, 'is_dev' => $is_dev];
208 return $manifests[$manifest_path];
209 }
210 function load_development_asset(object $manifest, string $entry, array $options, string $scope = ''): ?array
211 {
212 register_vite_client_script($manifest);
213 inject_react_refresh_preamble($manifest);
214 $dependencies = array_values(array_unique(array_merge([VITE_CLIENT_HANDLE], $options['dependencies'])));
215 $src = development_asset_src($manifest, $entry);
216 filter_script_tag($options['handle']);
217 if (!wp_register_script($options['handle'], $src, $dependencies, null, $options['in_footer'])) {
218 return null;
219 }
220 $assets = ['scripts' => [$options['handle']], 'styles' => $options['css_dependencies']];
221 return filter_value('development_assets', $assets, $scope, $manifest, $entry, $options);
222 }
223 function load_production_asset(object $manifest, string $entry, array $options, string $scope = ''): ?array
224 {
225 if (!isset($manifest->data->{$entry})) {
226 if (defined('WP_DEBUG') && \WP_DEBUG) {
227 wp_die(esc_html(sprintf('[vp-wp] Entry %s not found.', $entry)));
228 }
229 return null;
230 }
231 $item = $manifest->data->{$entry};
232 $url = asset_url($manifest->dir);
233 $assets = ['scripts' => [], 'styles' => []];
234 if (!$options['css_only']) {
235 filter_script_tag($options['handle']);
236 if (wp_register_script($options['handle'], join_asset_url($url, $item->file), $options['dependencies'], null, $options['in_footer'])) {
237 $assets['scripts'][] = $options['handle'];
238 }
239 }
240 if (!empty($item->imports)) {
241 $register_imports = static function (array $imports) use (&$register_imports, &$assets, $manifest, $options, $url): void {
242 foreach ($imports as $import) {
243 $import_item = $manifest->data->{$import};
244 if (!empty($import_item->imports)) {
245 $register_imports($import_item->imports);
246 }
247 if (!empty($import_item->css)) {
248 register_stylesheets($assets, $import_item->css, $url, $options);
249 }
250 }
251 };
252 $register_imports($item->imports);
253 }
254 if (!empty($item->css)) {
255 register_stylesheets($assets, $item->css, $url, $options);
256 }
257 return filter_value('production_assets', $assets, $scope, $manifest, $entry, $options);
258 }
259 /**
260 * Merge asset registration options with package defaults.
261 *
262 * @param array{
263 * handle?: string,
264 * dependencies?: list<string>,
265 * css_dependencies?: list<string>,
266 * css_media?: string,
267 * css_only?: bool,
268 * in_footer?: bool
269 * } $options Partial asset options.
270 * @return array{
271 * handle: string,
272 * dependencies: list<string>,
273 * css_dependencies: list<string>,
274 * css_media: string,
275 * css_only: bool,
276 * in_footer: bool
277 * } Normalized asset options.
278 */
279 function parse_options(array $options): array
280 {
281 return wp_parse_args($options, ['css_dependencies' => [], 'css_media' => 'all', 'css_only' => \false, 'dependencies' => [], 'handle' => '', 'in_footer' => \false]);
282 }
283 /**
284 * Generate a default WordPress handle from a manifest entry.
285 *
286 * @param string $entry Manifest entry key, such as `resources/app.ts`.
287 * @return string Sanitized lowercase handle.
288 */
289 function default_asset_handle(string $entry): string
290 {
291 return strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', pathinfo($entry, \PATHINFO_FILENAME)), '-'));
292 }
293 function register_stylesheets(array &$assets, array $stylesheets, string $url, array $options): void
294 {
295 foreach ($stylesheets as $stylesheet) {
296 $style_handle = stylesheet_handle($options['handle'], $stylesheet);
297 if (in_array($style_handle, $assets['styles'], \true)) {
298 continue;
299 }
300 if (wp_register_style($style_handle, join_asset_url($url, $stylesheet), $options['css_dependencies'], null, $options['css_media'])) {
301 $assets['styles'][] = $style_handle;
302 }
303 }
304 }
305 /**
306 * Generate a deterministic stylesheet handle for a built CSS asset.
307 *
308 * @param string $handle Base script or entry handle.
309 * @param string $stylesheet Relative stylesheet path from the manifest.
310 * @return string Stable stylesheet handle.
311 */
312 function stylesheet_handle(string $handle, string $stylesheet): string
313 {
314 $normalized_stylesheet = trim(wp_normalize_path($stylesheet), '/');
315 $slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', pathinfo($normalized_stylesheet, \PATHINFO_FILENAME)), '-'));
316 $hash = substr(md5($normalized_stylesheet), 0, 8);
317 return "{$handle}-{$slug}-{$hash}";
318 }
319 function register_vite_client_script(object $manifest): void
320 {
321 if (wp_script_is(VITE_CLIENT_HANDLE, 'registered')) {
322 return;
323 }
324 $src = development_asset_src($manifest, '@vite/client');
325 wp_register_script(VITE_CLIENT_HANDLE, $src, [], null, \false);
326 filter_script_tag(VITE_CLIENT_HANDLE);
327 }
328 function inject_react_refresh_preamble(object $manifest): void
329 {
330 static $did_print_preamble = \false;
331 if ($did_print_preamble || !should_inject_react_refresh($manifest->data)) {
332 return;
333 }
334 $script = <<<JS
335 import RefreshRuntime from "{$manifest->data->origin}/@react-refresh";
336 RefreshRuntime.injectIntoGlobalHook(window);
337 window.\$RefreshReg\$ = () => {};
338 window.\$RefreshSig\$ = () => (type) => type;
339 window.__vite_plugin_react_preamble_installed__ = true;
340 JS;
341 wp_add_inline_script(VITE_CLIENT_HANDLE, $script, 'after');
342 add_filter('wp_inline_script_attributes', static function (array $attributes): array {
343 if (isset($attributes['id']) && VITE_CLIENT_HANDLE . '-js-after' === $attributes['id']) {
344 $attributes['type'] = 'module';
345 }
346 return $attributes;
347 });
348 $did_print_preamble = \true;
349 }
350 function development_asset_src(object $manifest, string $entry): string
351 {
352 $base = trim(preg_replace('/[\/]{2,}/', '/', "{$manifest->data->base}/{$entry}"), '/');
353 return sprintf('%s/%s', untrailingslashit($manifest->data->origin), $base);
354 }
355 function filter_script_tag(string $handle): void
356 {
357 static $filtered_handles = [];
358 if (isset($filtered_handles[$handle])) {
359 return;
360 }
361 add_filter('script_loader_tag', static fn(...$args) => set_script_type_attribute($handle, ...$args), 10, 3);
362 $filtered_handles[$handle] = \true;
363 }
364 function set_script_type_attribute(string $target_handle, string $tag, string $handle, string $src): string
365 {
366 if ($target_handle !== $handle) {
367 return $tag;
368 }
369 $processor = new WP_HTML_Tag_Processor($tag);
370 while ($processor->next_tag('script')) {
371 if ($processor->get_attribute('src') === $src) {
372 $processor->set_attribute('type', 'module');
373 break;
374 }
375 }
376 return $processor->get_updated_html();
377 }
378 /**
379 * Build a public base URL for assets inside a manifest directory.
380 *
381 * The resulting URL is normalized so it works for assets located inside plugin
382 * or theme directories under `wp-content`.
383 *
384 * @param string $dir Absolute path to the manifest directory.
385 * @return string Public base URL for the manifest directory.
386 */
387 function prepare_asset_url(string $dir): string
388 {
389 $content_dir = wp_normalize_path(\WP_CONTENT_DIR);
390 $manifest_dir = wp_normalize_path($dir);
391 $url = content_url(str_replace($content_dir, '', $manifest_dir));
392 $url_matches_pattern = preg_match('/(?<address>http(?:s?):\/\/.*\/)(?<fullPath>wp-content(?<removablePath>\/.*)\/(?:plugins|themes)\/.*)/', $url, $url_parts);
393 if (0 === $url_matches_pattern) {
394 return $url;
395 }
396 ['address' => $address, 'fullPath' => $full_path, 'removablePath' => $removable_path] = $url_parts;
397 return sprintf('%s%s', $address, str_replace($removable_path, '', $full_path));
398 }
399 /**
400 * Apply both global and optional scoped filters for the runtime.
401 *
402 * @param string $hook Hook suffix, such as `manifest_data`.
403 * @param mixed $value Filtered value.
404 * @param string $scope Normalized hook scope.
405 * @param mixed ...$args Additional filter arguments.
406 * @return mixed Filtered value.
407 */
408 function filter_value(string $hook, $value, string $scope = '', ...$args)
409 {
410 $value = apply_filters("nabasa_vite_plus/{$hook}", $value, ...$args);
411 if ('' === $scope) {
412 return $value;
413 }
414 return apply_filters("nabasa_vite_plus/{$scope}/{$hook}", $value, ...$args);
415 }
416 /**
417 * Normalize a hook scope so it is safe to use in dynamic hook names.
418 *
419 * @param string $scope Raw hook scope.
420 * @return string Normalized scope.
421 */
422 function normalize_scope(string $scope): string
423 {
424 return sanitize_key($scope);
425 }
426 function should_inject_react_refresh(object $manifest_data): bool
427 {
428 if (!empty($manifest_data->reactRefresh)) {
429 return \true;
430 }
431 return isset($manifest_data->plugins) && is_array($manifest_data->plugins) && in_array('vite:react-refresh', $manifest_data->plugins, \true);
432 }
433 function string_ends_with(string $value, string $suffix): bool
434 {
435 if ('' === $suffix) {
436 return \true;
437 }
438 return substr($value, -strlen($suffix)) === $suffix;
439 }
440 /**
441 * Resolve the public URL for a file inside a manifest directory.
442 *
443 * @param string $manifest_dir Absolute path to the directory that contains built assets.
444 * @param string $asset Relative asset path inside the manifest directory.
445 * @return string Public asset URL.
446 */
447 function asset_url(string $manifest_dir, string $asset = ''): string
448 {
449 $base_url = prepare_asset_url($manifest_dir);
450 if ('' === $asset) {
451 return $base_url;
452 }
453 return join_asset_url($base_url, $asset);
454 }
455 /**
456 * Join a base asset URL with a relative asset path.
457 *
458 * @param string $base_url Base public URL.
459 * @param string $asset Relative asset path.
460 * @return string Combined asset URL.
461 */
462 function join_asset_url(string $base_url, string $asset): string
463 {
464 return sprintf('%s/%s', untrailingslashit($base_url), ltrim($asset, '/'));
465 }
466