viteHost = $serverConfig->host ?: $this->viteHost; $this->viteHostProtocol = $serverConfig->protocol ?: $this->viteHostProtocol; $this->vitePort = $serverConfig->port ?: $this->vitePort; } } add_filter('script_loader_tag', [$this, 'maybeConvertToModule'], 999, 3); } /** * Convert scripts from Vite dev server or built assets to ES modules */ public function maybeConvertToModule($tag, $handle, $src): string { $isViteScript = false; $assetBase = FLUENT_SUPPORT_PLUGIN_URL . 'assets/'; if (strpos($src, 'localhost:' . $this->vitePort) !== false || strpos($src, '@vite/client') !== false) { $isViteScript = true; } if (in_array($handle, $this->moduleScripts)) { $isViteScript = true; } if (!$this->shouldServeViaDevServer()) { $assetPatterns = [ $assetBase . 'admin/', $assetBase . 'portal/js/', ]; foreach ($assetPatterns as $pattern) { if (strpos($src, $pattern) !== false && strpos($src, '.js') !== false) { $excludePatterns = [ '/libs/', '/vendor/', 'purify.min.js', ]; $isExcluded = false; foreach ($excludePatterns as $exclude) { if (strpos($src, $exclude) !== false) { $isExcluded = true; break; } } if (!$isExcluded) { $isViteScript = true; break; } } } } if ($isViteScript) { if (strpos($tag, 'type="module"') !== false || strpos($tag, "type='module'") !== false) { return $tag; } $tag = preg_replace('/usingDevMode() || !static::$instance->isViteServerRunning()) { (static::$instance)->loadViteManifest(); } } return static::$instance; } private function loadViteManifest() { if (!empty($this->manifestData)) { return; } $manifestPath = FLUENT_SUPPORT_PLUGIN_PATH . 'config' . DIRECTORY_SEPARATOR . 'vite_config.php'; if (file_exists($manifestPath)) { $this->manifestData = require $manifestPath; } if (empty($this->manifestData)) { $this->manifestData = []; } } public static function enqueueScript($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite { return static::getInstance()->enqueue_script( $handle, $src, $dependency, $version, $inFooter ); } private function enqueue_script($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite { $this->moduleScripts[] = $handle; $this->lastJsHandle = $handle; if (!$this->isScriptFilterAdded) { add_filter('script_loader_tag', function ($tag, $handle, $src) { return $this->addModuleToScript($tag, $handle, $src); }, 10, 3); $this->isScriptFilterAdded = true; } if ($this->shouldServeViaDevServer()) { $srcPath = $this->getVitePath() . $src; } else { $assetFile = $this->getFileFromManifest($src); $srcPath = $this->getProductionFilePath($assetFile); } if (empty($srcPath)) { return $this; } $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version; wp_enqueue_script($handle, $srcPath, $dependency, $version, $inFooter); return $this; } private function addModuleToScript($tag, $handle, $src): string { if (in_array($handle, $this->moduleScripts)) { if ($this->usingDevMode() || strpos($src, 'assets/') !== false) { $tag = '' . "\n"; } else { $tag = '' . "\n"; } } return $tag; } private function getFileFromManifest($src) { if (isset($this->manifestData[$this->resourceDirectory . $src])) { return $this->manifestData[$this->resourceDirectory . $src]; } return ''; } private function getProductionFilePath($file): string { if (!isset($file['file'])) { return ''; } $assetPath = static::getAssetPath(); $this->ensureChunkCssIsLoaded($file); return ($assetPath . $file['file']); } // CSS files that are standalone entry outputs (NOT merged into style.css). // Everything else from Vite chunk splitting is merged into admin/css/alpha-admin.css. private array $standaloneCssPaths = [ 'admin/css/alpha-admin.css', 'portal/css/app.css', 'admin/css/all_public.css', ]; private function ensureChunkCssIsLoaded($file) { $assetPath = static::getAssetPath(); $cssFiles = $this->collectChunkCssFiles($file); foreach ($cssFiles as $cssPath) { if (isset($this->enqueuedChunkCss[$cssPath])) { continue; } // Skip chunk CSS that was merged into style.css by mergeCssChunksPlugin. // Only standalone SCSS entry outputs are NOT merged. if (!in_array($cssPath, $this->standaloneCssPaths)) { $this->enqueuedChunkCss[$cssPath] = true; continue; } wp_enqueue_style( 'fluent_support_vite_css_' . md5($cssPath), $assetPath . $cssPath, [], FLUENT_SUPPORT_VERSION ); $this->enqueuedChunkCss[$cssPath] = true; } } private function collectChunkCssFiles($file, &$visited = []): array { $cssFiles = []; if (!is_array($file)) { return $cssFiles; } $fileId = isset($file['file']) ? $file['file'] : md5(wp_json_encode($file)); if (isset($visited[$fileId])) { return $cssFiles; } $visited[$fileId] = true; if (isset($file['css']) && is_array($file['css'])) { foreach ($file['css'] as $path) { if (is_string($path) && $path !== '') { $cssFiles[] = $path; } } } if (isset($file['imports']) && is_array($file['imports'])) { foreach ($file['imports'] as $importKey) { if (!isset($this->manifestData[$importKey]) || !is_array($this->manifestData[$importKey])) { continue; } $cssFiles = array_merge($cssFiles, $this->collectChunkCssFiles($this->manifestData[$importKey], $visited)); } } return array_values(array_unique($cssFiles)); } public static function enqueueStyle($handle, $src, $dependency = [], $version = null, $media = 'all') { static::getInstance()->enqueue_style( $handle, $src, $dependency, $version, $media ); } private function enqueue_style($handle, $src, $dependency = [], $version = null, $media = 'all') { if ($this->shouldServeViaDevServer()) { $srcPath = $this->getVitePath() . $src; } else { $assetFile = $this->getFileFromManifest($src); $srcPath = $this->getProductionFilePath($assetFile); } if (empty($srcPath)) { return; } $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version; wp_enqueue_style($handle, $srcPath, $dependency, $version, $media); } public static function enqueueStaticScript($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite { $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version; return static::getInstance()->enqueue_static_script( $handle, $src, $dependency, $version, $inFooter ); } private function enqueue_static_script($handle, $src, $dependency = [], $version = null, $inFooter = false): Vite { $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version; wp_enqueue_script( $handle, $this->getStaticEnqueuePath($src), $dependency, $version, $inFooter ); return $this; } private function getStaticEnqueuePath($path): string { if ($this->shouldServeViaDevServer()) { return $this->getVitePath() . $path; } return $this->get_asset_url($path); } public static function enqueueStaticStyle($handle, $src, $dependency = [], $version = null, $media = 'all') { $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version; static::getInstance()->enqueue_static_style( $handle, $src, $dependency, $version, $media ); } private function enqueue_static_style($handle, $src, $dependency = [], $version = null, $media = 'all') { $version = empty($version) ? FLUENT_SUPPORT_VERSION : $version; wp_enqueue_style( $handle, $this->getStaticEnqueuePath($src), $dependency, $version, $media ); } public static function underDevelopment(): bool { return static::getInstance()->usingDevMode(); } /** * True only when env=dev AND the Vite dev server is actually reachable. * Use this instead of underDevelopment() when deciding whether to skip built assets. */ public static function isServingFromDevServer(): bool { return static::getInstance()->shouldServeViaDevServer(); } public function usingDevMode(): bool { $app = App::getInstance(); return $app->config->get('app.env') === 'dev'; } /** * True only when env=dev AND the Vite dev server is reachable. */ private function shouldServeViaDevServer(): bool { return $this->usingDevMode() && $this->isViteServerRunning(); } private function isViteServerRunning(): bool { static $isRunning = null; if ($isRunning !== null) { return $isRunning; } $viteUrl = $this->viteHostProtocol . $this->viteHost . ':' . $this->vitePort . '/@vite/client'; $response = wp_remote_get($viteUrl, [ 'timeout' => 1, 'sslverify' => false ]); $isRunning = !is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200; return $isRunning; } public function getVitePath(): string { $protocol = rtrim($this->viteHostProtocol, ':/'); $host = rtrim($this->viteHost, '/'); $port = $this->vitePort; $resource = ltrim($this->resourceDirectory, '/'); return sprintf('%s://%s:%s/%s', $protocol, $host, $port, $resource); } /** * Resolve asset path - works for both dev and production. * Accepts Mix-style paths (e.g. admin/js/start.js) and maps them to Vite source paths. */ public static function getEnqueuePath($path = ''): string { $vite = static::getInstance(); $path = ltrim($path, '/'); if (!$vite->usingDevMode()) { $sourcePath = $vite->getSourcePathForManifest($path); $assetFile = $vite->getFileFromManifest($sourcePath); if ($assetFile) { $srcPath = $vite->getProductionFilePath($assetFile); } else { $srcPath = static::getAssetPath() . $path; } } else { if ($vite->isViteServerRunning()) { $srcPath = $vite->mapToSourcePath($path); } else { $sourcePath = $vite->getSourcePathForManifest($path); $assetFile = $vite->getFileFromManifest($sourcePath); if ($assetFile) { $srcPath = $vite->getProductionFilePath($assetFile); } else { $srcPath = static::getAssetPath() . $path; } } } return $srcPath; } /** * Map built asset paths to source paths for dev mode */ private function mapToSourcePath($path): string { $cssMap = [ 'admin/css/alpha-admin.css' => 'scss/alpha-admin.scss', 'portal/css/app.css' => 'customer_portal/app.scss', 'admin/css/all_public.css' => 'scss/all_public.scss', ]; $jsMap = [ 'admin/js/start.js' => 'admin/start.js', 'admin/js/global_admin.js' => 'admin/global_admin.js', 'admin/js/global_summary.js' => 'admin/global_summary.js', 'portal/js/app.js' => 'customer_portal/portal.js', 'portal/js/login_helper.js' => 'customer_portal/login_helper.js', ]; $pathMap = array_merge($cssMap, $jsMap); if (isset($pathMap[$path])) { return $this->getVitePath() . $pathMap[$path]; } if (strpos($path, 'resources/') === 0) { return $this->getVitePath() . substr($path, 10); } return $this->getVitePath() . $path; } /** * Map Mix-style paths to Vite source paths for manifest lookup */ private function getSourcePathForManifest($path): string { $pathMap = [ 'admin/js/start.js' => 'admin/start.js', 'admin/js/global_admin.js' => 'admin/global_admin.js', 'admin/js/global_summary.js' => 'admin/global_summary.js', 'portal/js/app.js' => 'customer_portal/portal.js', 'portal/js/login_helper.js' => 'customer_portal/login_helper.js', 'admin/css/alpha-admin.css' => 'scss/alpha-admin.scss', 'portal/css/app.css' => 'customer_portal/app.scss', 'admin/css/all_public.css' => 'scss/all_public.scss', ]; if (isset($pathMap[$path])) { return $pathMap[$path]; } return $path; } public static function getAssetUrl($path = ''): string { return esc_url(static::getInstance()->get_asset_url($path) ?? ''); } private function get_asset_url($path = ''): string { if ($this->shouldServeViaDevServer()) { return $this->getVitePath() . $path; } return FLUENT_SUPPORT_PLUGIN_URL . 'assets/' . ltrim($path, '/'); } public static function getAssetPath(): string { return FLUENT_SUPPORT_PLUGIN_URL . 'assets/'; } /** * Inject Vite client for HMR in development mode */ public static function injectViteClient() { $vite = static::getInstance(); if ($vite->shouldServeViaDevServer()) { $protocol = rtrim($vite->viteHostProtocol, ':/'); $host = rtrim($vite->viteHost, '/'); $port = $vite->vitePort; $viteClientUrl = sprintf('%s://%s:%s/@vite/client', $protocol, $host, $port); echo '' . "\n"; } } }