- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
306 lines
8.4 KiB
PHP
306 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Framework\Vite;
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\CacheKey;
|
|
use App\Framework\Core\PathProvider;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Process\Services\TcpPortCheckService;
|
|
use App\Framework\Vite\Exceptions\ViteManifestException;
|
|
use App\Framework\Vite\ValueObjects\ViteConfig;
|
|
use App\Framework\Vite\ValueObjects\ViteEntry;
|
|
use App\Framework\Vite\ValueObjects\ViteManifest;
|
|
|
|
/**
|
|
* Vite Service for Asset Management
|
|
*
|
|
* Manages Vite assets for both development (with HMR) and production (from manifest).
|
|
* Automatically detects dev server availability and generates appropriate HTML tags.
|
|
*/
|
|
final readonly class ViteService
|
|
{
|
|
private const DEV_SERVER_TIMEOUT = 1;
|
|
private const MANIFEST_CACHE_KEY = 'vite:manifest';
|
|
private const MANIFEST_CACHE_TTL_SECONDS = 3600;
|
|
|
|
public function __construct(
|
|
private ViteConfig $config,
|
|
private PathProvider $pathProvider,
|
|
private Cache $cache,
|
|
private TcpPortCheckService $portCheck
|
|
) {}
|
|
|
|
/**
|
|
* Check if Vite dev server is running
|
|
*/
|
|
public function isDevServerRunning(): bool
|
|
{
|
|
if (! $this->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<string> $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<string> $entrypoints
|
|
*/
|
|
private function getDevTags(array $entrypoints): string
|
|
{
|
|
$tags = [];
|
|
$nonce = $this->getNonceAttribute();
|
|
|
|
// Vite client for HMR
|
|
$tags[] = sprintf(
|
|
'<script type="module" src="%s"%s></script>',
|
|
$this->config->getViteClientUrl(),
|
|
$nonce
|
|
);
|
|
|
|
// Entrypoint scripts
|
|
foreach ($entrypoints as $entry) {
|
|
$entryPath = 'resources/js/' . $entry . '.js';
|
|
$url = $this->config->getDevServerEntry($entryPath);
|
|
|
|
$tags[] = sprintf(
|
|
'<script type="module" src="%s"%s></script>',
|
|
$url,
|
|
$nonce
|
|
);
|
|
}
|
|
|
|
return implode("\n", $tags);
|
|
}
|
|
|
|
/**
|
|
* Generate tags for production mode
|
|
*
|
|
* @param array<string> $entrypoints
|
|
*/
|
|
private function getProductionTags(array $entrypoints): string
|
|
{
|
|
$manifest = $this->getManifest();
|
|
|
|
if ($manifest === null) {
|
|
return '<!-- Vite manifest not found. Run "npm run build" -->';
|
|
}
|
|
|
|
$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(
|
|
'<link rel="stylesheet" href="%s">',
|
|
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(
|
|
'<script type="module" src="%s"%s%s></script>',
|
|
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<string> $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(
|
|
'<link rel="preload" href="%s" as="%s">',
|
|
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);
|
|
}
|
|
}
|