PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.2.1
Fluent Support – Helpdesk & Customer Support Ticket System v2.2.1
2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 All 67 releases
fluent-support / app / Vite.php

Vite.php in Fluent Support – Helpdesk & Customer Support Ticket System 2.2.1, at app/Vite.php

520 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\App;
4
5 class Vite
6 {
7 private array $moduleScripts = [];
8 private bool $isScriptFilterAdded = false;
9 private string $viteHostProtocol = 'http://';
10 private string $viteHost = 'localhost';
11 private string $vitePort = '5175';
12 private string $resourceDirectory = 'resources/';
13
14 protected static ?Vite $instance = null;
15 public ?string $lastJsHandle = null;
16 private ?array $manifestData = null;
17 private array $enqueuedChunkCss = [];
18
19 public function __construct()
20 {
21 $serverConfigPath = FLUENT_SUPPORT_PLUGIN_PATH . 'config' . DIRECTORY_SEPARATOR . 'vite.json';
22 if (file_exists($serverConfigPath)) {
23 $serverConfig = json_decode(file_get_contents($serverConfigPath));
24 if ($serverConfig) {
25 $this->viteHost = $serverConfig->host ?: $this->viteHost;
26 $this->viteHostProtocol = $serverConfig->protocol ?: $this->viteHostProtocol;
27 $this->vitePort = $serverConfig->port ?: $this->vitePort;
28 }
29 }
30
31 add_filter('script_loader_tag', [$this, 'maybeConvertToModule'], 999, 3);
32 }
33
34 /**
35 * Convert scripts from Vite dev server or built assets to ES modules
36 */
37 public function maybeConvertToModule($tag, $handle, $src): string
38 {
39 $isViteScript = false;
40 $assetBase = FLUENT_SUPPORT_PLUGIN_URL . 'assets/';
41
42 if (strpos($src, 'localhost:' . $this->vitePort) !== false || strpos($src, '@vite/client') !== false) {
43 $isViteScript = true;
44 }
45
46 if (in_array($handle, $this->moduleScripts)) {
47 $isViteScript = true;
48 }
49
50 if (!$this->shouldServeViaDevServer()) {
51 $assetPatterns = [
52 $assetBase . 'admin/',
53 $assetBase . 'portal/js/',
54 ];
55 foreach ($assetPatterns as $pattern) {
56 if (strpos($src, $pattern) !== false && strpos($src, '.js') !== false) {
57 $excludePatterns = [
58 '/libs/',
59 '/vendor/',
60 'purify.min.js',
61 ];
62 $isExcluded = false;
63 foreach ($excludePatterns as $exclude) {
64 if (strpos($src, $exclude) !== false) {
65 $isExcluded = true;
66 break;
67 }
68 }
69 if (!$isExcluded) {
70 $isViteScript = true;
71 break;
72 }
73 }
74 }
75 }
76
77 if ($isViteScript) {
78 if (strpos($tag, 'type="module"') !== false || strpos($tag, "type='module'") !== false) {
79 return $tag;
80 }
81 $tag = preg_replace('/<script\s+/', '<script type="module" crossorigin ', $tag);
82 }
83
84 return $tag;
85 }
86
87 private static function getInstance(): Vite
88 {
89 if (static::$instance === null) {
90 static::$instance = new static();
91 if (!static::$instance->usingDevMode() || !static::$instance->isViteServerRunning()) {
92 (static::$instance)->loadViteManifest();
93 }
94 }
95
96 return static::$instance;
97 }
98
99 private function loadViteManifest()
100 {
101 if (!empty($this->manifestData)) {
102 return;
103 }
104
105 $manifestPath = FLUENT_SUPPORT_PLUGIN_PATH . 'config' . DIRECTORY_SEPARATOR . 'vite_config.php';
106
107 if (file_exists($manifestPath)) {
108 $this->manifestData = require $manifestPath;
109 }
110
111 if (empty($this->manifestData)) {
112 $this->manifestData = [];
113 }
114 }
115
116 public static function enqueueScript($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite
117 {
118 return static::getInstance()->enqueue_script(
119 $handle, $src, $dependency, $version, $inFooter
120 );
121 }
122
123 private function enqueue_script($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite
124 {
125 $this->moduleScripts[] = $handle;
126 $this->lastJsHandle = $handle;
127
128 if (!$this->isScriptFilterAdded) {
129 add_filter('script_loader_tag', function ($tag, $handle, $src) {
130 return $this->addModuleToScript($tag, $handle, $src);
131 }, 10, 3);
132 $this->isScriptFilterAdded = true;
133 }
134
135 if ($this->shouldServeViaDevServer()) {
136 $srcPath = $this->getVitePath() . $src;
137 } else {
138 $assetFile = $this->getFileFromManifest($src);
139 $srcPath = $this->getProductionFilePath($assetFile);
140 }
141
142 if (empty($srcPath)) {
143 return $this;
144 }
145
146 $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version;
147
148 wp_enqueue_script($handle, $srcPath, $dependency, $version, $inFooter);
149
150 return $this;
151 }
152
153 private function addModuleToScript($tag, $handle, $src): string
154 {
155 if (in_array($handle, $this->moduleScripts)) {
156 if ($this->usingDevMode() || strpos($src, 'assets/') !== false) {
157 $tag = '<script type="module" crossorigin src="' . esc_url($src) . '" id="' . esc_attr($handle) . '-js"></script>' . "\n";
158 } else {
159 $tag = '<script type="module" src="' . esc_url($src) . '" id="' . esc_attr($handle) . '-js"></script>' . "\n";
160 }
161 }
162 return $tag;
163 }
164
165 private function getFileFromManifest($src)
166 {
167 if (isset($this->manifestData[$this->resourceDirectory . $src])) {
168 return $this->manifestData[$this->resourceDirectory . $src];
169 }
170
171 return '';
172 }
173
174 private function getProductionFilePath($file): string
175 {
176 if (!isset($file['file'])) {
177 return '';
178 }
179
180 $assetPath = static::getAssetPath();
181 $this->ensureChunkCssIsLoaded($file);
182
183 return ($assetPath . $file['file']);
184 }
185
186 // CSS files that are standalone entry outputs (NOT merged into style.css).
187 // Everything else from Vite chunk splitting is merged into admin/css/alpha-admin.css.
188 private array $standaloneCssPaths = [
189 'admin/css/alpha-admin.css',
190 'portal/css/app.css',
191 'admin/css/all_public.css',
192 ];
193
194 private function ensureChunkCssIsLoaded($file)
195 {
196 $assetPath = static::getAssetPath();
197 $cssFiles = $this->collectChunkCssFiles($file);
198
199 foreach ($cssFiles as $cssPath) {
200 if (isset($this->enqueuedChunkCss[$cssPath])) {
201 continue;
202 }
203
204 // Skip chunk CSS that was merged into style.css by mergeCssChunksPlugin.
205 // Only standalone SCSS entry outputs are NOT merged.
206 if (!in_array($cssPath, $this->standaloneCssPaths)) {
207 $this->enqueuedChunkCss[$cssPath] = true;
208 continue;
209 }
210
211 wp_enqueue_style(
212 'fluent_support_vite_css_' . md5($cssPath),
213 $assetPath . $cssPath,
214 [],
215 FLUENT_SUPPORT_VERSION
216 );
217
218 $this->enqueuedChunkCss[$cssPath] = true;
219 }
220 }
221
222 private function collectChunkCssFiles($file, &$visited = []): array
223 {
224 $cssFiles = [];
225
226 if (!is_array($file)) {
227 return $cssFiles;
228 }
229
230 $fileId = isset($file['file']) ? $file['file'] : md5(wp_json_encode($file));
231 if (isset($visited[$fileId])) {
232 return $cssFiles;
233 }
234 $visited[$fileId] = true;
235
236 if (isset($file['css']) && is_array($file['css'])) {
237 foreach ($file['css'] as $path) {
238 if (is_string($path) && $path !== '') {
239 $cssFiles[] = $path;
240 }
241 }
242 }
243
244 if (isset($file['imports']) && is_array($file['imports'])) {
245 foreach ($file['imports'] as $importKey) {
246 if (!isset($this->manifestData[$importKey]) || !is_array($this->manifestData[$importKey])) {
247 continue;
248 }
249 $cssFiles = array_merge($cssFiles, $this->collectChunkCssFiles($this->manifestData[$importKey], $visited));
250 }
251 }
252
253 return array_values(array_unique($cssFiles));
254 }
255
256 public static function enqueueStyle($handle, $src, $dependency = [], $version = null, $media = 'all')
257 {
258 static::getInstance()->enqueue_style(
259 $handle, $src, $dependency, $version, $media
260 );
261 }
262
263 private function enqueue_style($handle, $src, $dependency = [], $version = null, $media = 'all')
264 {
265 if ($this->shouldServeViaDevServer()) {
266 $srcPath = $this->getVitePath() . $src;
267 } else {
268 $assetFile = $this->getFileFromManifest($src);
269 $srcPath = $this->getProductionFilePath($assetFile);
270 }
271
272 if (empty($srcPath)) {
273 return;
274 }
275
276 $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version;
277
278 wp_enqueue_style($handle, $srcPath, $dependency, $version, $media);
279 }
280
281 public static function enqueueStaticScript($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite
282 {
283 $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version;
284
285 return static::getInstance()->enqueue_static_script(
286 $handle, $src, $dependency, $version, $inFooter
287 );
288 }
289
290 private function enqueue_static_script($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite
291 {
292 $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version;
293
294 wp_enqueue_script(
295 $handle,
296 $this->getStaticEnqueuePath($src),
297 $dependency,
298 $version,
299 $inFooter
300 );
301
302 return $this;
303 }
304
305 private function getStaticEnqueuePath($path): string
306 {
307 if ($this->shouldServeViaDevServer()) {
308 return $this->getVitePath() . $path;
309 }
310
311 return $this->get_asset_url($path);
312 }
313
314 public static function enqueueStaticStyle($handle, $src, $dependency = [], $version = null, $media = 'all')
315 {
316 $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version;
317
318 static::getInstance()->enqueue_static_style(
319 $handle, $src, $dependency, $version, $media
320 );
321 }
322
323 private function enqueue_static_style($handle, $src, $dependency = [], $version = null, $media = 'all')
324 {
325 $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version;
326
327 wp_enqueue_style(
328 $handle,
329 $this->getStaticEnqueuePath($src),
330 $dependency,
331 $version,
332 $media
333 );
334 }
335
336 public static function underDevelopment(): bool
337 {
338 return static::getInstance()->usingDevMode();
339 }
340
341 /**
342 * True only when env=dev AND the Vite dev server is actually reachable.
343 * Use this instead of underDevelopment() when deciding whether to skip built assets.
344 */
345 public static function isServingFromDevServer(): bool
346 {
347 return static::getInstance()->shouldServeViaDevServer();
348 }
349
350 public function usingDevMode(): bool
351 {
352 $app = App::getInstance();
353 return $app->config->get('app.env') === 'dev';
354 }
355
356 /**
357 * True only when env=dev AND the Vite dev server is reachable.
358 */
359 private function shouldServeViaDevServer(): bool
360 {
361 return $this->usingDevMode() && $this->isViteServerRunning();
362 }
363
364 private function isViteServerRunning(): bool
365 {
366 static $isRunning = null;
367
368 if ($isRunning !== null) {
369 return $isRunning;
370 }
371
372 $viteUrl = $this->viteHostProtocol . $this->viteHost . ':' . $this->vitePort . '/@vite/client';
373
374 $response = wp_remote_get($viteUrl, [
375 'timeout' => 1,
376 'sslverify' => false
377 ]);
378
379 $isRunning = !is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200;
380
381 return $isRunning;
382 }
383
384 public function getVitePath(): string
385 {
386 $protocol = rtrim($this->viteHostProtocol, ':/');
387 $host = rtrim($this->viteHost, '/');
388 $port = $this->vitePort;
389 $resource = ltrim($this->resourceDirectory, '/');
390
391 return sprintf('%s://%s:%s/%s', $protocol, $host, $port, $resource);
392 }
393
394 /**
395 * Resolve asset path - works for both dev and production.
396 * Accepts Mix-style paths (e.g. admin/js/start.js) and maps them to Vite source paths.
397 */
398 public static function getEnqueuePath($path = ''): string
399 {
400 $vite = static::getInstance();
401 $path = ltrim($path, '/');
402
403 if (!$vite->usingDevMode()) {
404 $sourcePath = $vite->getSourcePathForManifest($path);
405 $assetFile = $vite->getFileFromManifest($sourcePath);
406 if ($assetFile) {
407 $srcPath = $vite->getProductionFilePath($assetFile);
408 } else {
409 $srcPath = static::getAssetPath() . $path;
410 }
411 } else {
412 if ($vite->isViteServerRunning()) {
413 $srcPath = $vite->mapToSourcePath($path);
414 } else {
415 $sourcePath = $vite->getSourcePathForManifest($path);
416 $assetFile = $vite->getFileFromManifest($sourcePath);
417 if ($assetFile) {
418 $srcPath = $vite->getProductionFilePath($assetFile);
419 } else {
420 $srcPath = static::getAssetPath() . $path;
421 }
422 }
423 }
424
425 return $srcPath;
426 }
427
428 /**
429 * Map built asset paths to source paths for dev mode
430 */
431 private function mapToSourcePath($path): string
432 {
433 $cssMap = [
434 'admin/css/alpha-admin.css' => 'scss/alpha-admin.scss',
435 'portal/css/app.css' => 'customer_portal/app.scss',
436 'admin/css/all_public.css' => 'scss/all_public.scss',
437 ];
438
439 $jsMap = [
440 'admin/js/start.js' => 'admin/start.js',
441 'admin/js/global_admin.js' => 'admin/global_admin.js',
442 'admin/js/global_summary.js' => 'admin/global_summary.js',
443 'portal/js/app.js' => 'customer_portal/portal.js',
444 'portal/js/login_helper.js' => 'customer_portal/login_helper.js',
445 ];
446
447 $pathMap = array_merge($cssMap, $jsMap);
448
449 if (isset($pathMap[$path])) {
450 return $this->getVitePath() . $pathMap[$path];
451 }
452
453 if (strpos($path, 'resources/') === 0) {
454 return $this->getVitePath() . substr($path, 10);
455 }
456
457 return $this->getVitePath() . $path;
458 }
459
460 /**
461 * Map Mix-style paths to Vite source paths for manifest lookup
462 */
463 private function getSourcePathForManifest($path): string
464 {
465 $pathMap = [
466 'admin/js/start.js' => 'admin/start.js',
467 'admin/js/global_admin.js' => 'admin/global_admin.js',
468 'admin/js/global_summary.js' => 'admin/global_summary.js',
469 'portal/js/app.js' => 'customer_portal/portal.js',
470 'portal/js/login_helper.js' => 'customer_portal/login_helper.js',
471
472 'admin/css/alpha-admin.css' => 'scss/alpha-admin.scss',
473 'portal/css/app.css' => 'customer_portal/app.scss',
474 'admin/css/all_public.css' => 'scss/all_public.scss',
475 ];
476
477 if (isset($pathMap[$path])) {
478 return $pathMap[$path];
479 }
480
481 return $path;
482 }
483
484 public static function getAssetUrl($path = ''): string
485 {
486 return esc_url(static::getInstance()->get_asset_url($path) ?? '');
487 }
488
489 private function get_asset_url($path = ''): string
490 {
491 if ($this->shouldServeViaDevServer()) {
492 return $this->getVitePath() . $path;
493 }
494
495 return FLUENT_SUPPORT_PLUGIN_URL . 'assets/' . ltrim($path, '/');
496 }
497
498 public static function getAssetPath(): string
499 {
500 return FLUENT_SUPPORT_PLUGIN_URL . 'assets/';
501 }
502
503 /**
504 * Inject Vite client for HMR in development mode
505 */
506 public static function injectViteClient()
507 {
508 $vite = static::getInstance();
509
510 if ($vite->shouldServeViaDevServer()) {
511 $protocol = rtrim($vite->viteHostProtocol, ':/');
512 $host = rtrim($vite->viteHost, '/');
513 $port = $vite->vitePort;
514
515 $viteClientUrl = sprintf('%s://%s:%s/@vite/client', $protocol, $host, $port);
516 echo '<script type="module" crossorigin src="' . esc_url($viteClientUrl) . '"></script>' . "\n";
517 }
518 }
519 }
520