config->hotReload) { return false; } // Parse dev server URL to get host and port $url = parse_url($this->config->devServerUrl); if ($url === false || ! isset($url['host'])) { return false; } $host = $url['host']; $port = $url['port'] ?? ($url['scheme'] === 'https' ? 443 : 80); $isHttps = ($url['scheme'] ?? 'http') === 'https'; // Use TcpPortCheckService for port checking (no external dependencies) if ($isHttps) { return $this->portCheck->isSslPortOpen($host, $port, self::DEV_SERVER_TIMEOUT); } return $this->portCheck->isPortOpen($host, $port, self::DEV_SERVER_TIMEOUT); } /** * Get Vite manifest (cached in production) */ public function getManifest(): ?ViteManifest { // In dev mode, skip manifest if ($this->isDevServerRunning()) { return null; } // Try cache first $cacheKey = CacheKey::fromString(self::MANIFEST_CACHE_KEY); $cached = $this->cache->get($cacheKey); if ($cached !== null && $cached->value instanceof ViteManifest) { return $cached->value; } // Load from file $publicPath = $this->pathProvider->resolvePath('/public'); $manifestPath = $this->config->getManifestPath($publicPath); try { $manifest = ViteManifest::fromFile($manifestPath); // Cache for 1 hour $cacheItem = CacheItem::forSet( key: $cacheKey, value: $manifest, ttl: Duration::fromSeconds(self::MANIFEST_CACHE_TTL_SECONDS) ); $this->cache->set($cacheItem); return $manifest; } catch (ViteManifestException) { return null; } } /** * Generate HTML tags for given entrypoints * * @param array $entrypoints */ public function getTags(array $entrypoints = []): string { $entries = ! empty($entrypoints) ? $entrypoints : $this->config->entrypoints; if ($this->isDevServerRunning()) { return $this->getDevTags($entries); } return $this->getProductionTags($entries); } /** * Generate tags for development mode * * @param array $entrypoints */ private function getDevTags(array $entrypoints): string { $tags = []; $nonce = $this->getNonceAttribute(); // Vite client for HMR $tags[] = sprintf( '', $this->config->getViteClientUrl(), $nonce ); // Entrypoint scripts foreach ($entrypoints as $entry) { $entryPath = 'resources/js/' . $entry . '.js'; $url = $this->config->getDevServerEntry($entryPath); $tags[] = sprintf( '', $url, $nonce ); } return implode("\n", $tags); } /** * Generate tags for production mode * * @param array $entrypoints */ private function getProductionTags(array $entrypoints): string { $manifest = $this->getManifest(); if ($manifest === null) { return ''; } $tags = []; $processedEntries = []; foreach ($entrypoints as $entryName) { $entryPath = 'resources/js/' . $entryName . '.js'; $entries = $manifest->getEntryWithDependencies($entryPath); foreach ($entries as $entry) { // Avoid duplicates if (in_array($entry->file, $processedEntries, true)) { continue; } $processedEntries[] = $entry->file; // Generate CSS tags foreach ($entry->css as $cssFile) { $tags[] = $this->generateCssTag($cssFile); } // Generate script tag $tags[] = $this->generateScriptTag($entry); } } return implode("\n", $tags); } /** * Generate CSS link tag */ private function generateCssTag(string $cssFile): string { $url = '/' . ltrim($cssFile, '/'); return sprintf( '', htmlspecialchars($url, ENT_QUOTES, 'UTF-8') ); } /** * Generate script tag */ private function generateScriptTag(ViteEntry $entry): string { $url = '/' . ltrim($entry->file, '/'); $nonce = $this->getNonceAttribute(); $integrity = $entry->integrity ? sprintf(' integrity="%s"', $entry->integrity) : ''; return sprintf( '', htmlspecialchars($url, ENT_QUOTES, 'UTF-8'), $integrity, $nonce ); } /** * Get CSP nonce attribute if configured */ private function getNonceAttribute(): string { if ($this->config->nonce === null) { return ''; } return sprintf(' nonce="%s"', htmlspecialchars($this->config->nonce, ENT_QUOTES, 'UTF-8')); } /** * Preload assets for given entrypoints * * @param array $entrypoints */ public function getPreloadTags(array $entrypoints = []): string { // Preloading only makes sense in production if ($this->isDevServerRunning()) { return ''; } $entries = ! empty($entrypoints) ? $entrypoints : $this->config->entrypoints; $manifest = $this->getManifest(); if ($manifest === null) { return ''; } $tags = []; $processedFiles = []; foreach ($entries as $entryName) { $entryPath = 'resources/js/' . $entryName . '.js'; $mainEntry = $manifest->getEntry($entryPath); if ($mainEntry === null) { continue; } // Preload main JS if (! in_array($mainEntry->file, $processedFiles, true)) { $processedFiles[] = $mainEntry->file; $tags[] = $this->generatePreloadTag($mainEntry->file, 'script'); } // Preload CSS foreach ($mainEntry->css as $cssFile) { if (! in_array($cssFile, $processedFiles, true)) { $processedFiles[] = $cssFile; $tags[] = $this->generatePreloadTag($cssFile, 'style'); } } } return implode("\n", $tags); } /** * Generate preload link tag */ private function generatePreloadTag(string $file, string $as): string { $url = '/' . ltrim($file, '/'); return sprintf( '', htmlspecialchars($url, ENT_QUOTES, 'UTF-8'), $as ); } /** * Clear manifest cache */ public function clearManifestCache(): bool { $cacheKey = CacheKey::fromString(self::MANIFEST_CACHE_KEY); return $this->cache->forget($cacheKey); } }