Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ViteManifest
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 10
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 load
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 exists
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getEntry
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getJsUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getCssUrls
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPreloadUrls
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getEntryNames
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 hasEntry
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 clearCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace BuyerKiosk\Vite;
6
7/**
8 * Vite Manifest Reader
9 *
10 * Reads and parses the Vite build manifest.json to resolve
11 * hashed asset filenames for production builds.
12 *
13 * Deployment Model: Committed Artifacts
14 * - Build outputs are committed to git
15 * - Manifest.json maps entry names to hashed filenames
16 * - No Node.js required on production servers
17 */
18class ViteManifest
19{
20    /**
21     * Path to manifest.json file
22     */
23    private string $manifestPath;
24
25    /**
26     * Cached manifest data
27     */
28    private ?array $manifest = null;
29
30    /**
31     * Base URL for assets (e.g., /js/dist/)
32     */
33    private string $baseUrl;
34
35    /**
36     * @param string $manifestPath Absolute path to manifest.json
37     * @param string $baseUrl Base URL for asset URLs (default: /js/dist/)
38     */
39    public function __construct(string $manifestPath, string $baseUrl = '/js/dist/')
40    {
41        $this->manifestPath = $manifestPath;
42        $this->baseUrl = rtrim($baseUrl, '/') . '/';
43    }
44
45    /**
46     * Load and parse the manifest file
47     *
48     * @return array The parsed manifest
49     * @throws ViteManifestException If manifest cannot be read or parsed
50     */
51    public function load(): array
52    {
53        if ($this->manifest !== null) {
54            return $this->manifest;
55        }
56
57        if (!file_exists($this->manifestPath)) {
58            throw new ViteManifestException(
59                "Vite manifest not found at: {$this->manifestPath}" .
60                "Run 'npm run build' to generate it."
61            );
62        }
63
64        $contents = file_get_contents($this->manifestPath);
65        if ($contents === false) {
66            throw new ViteManifestException(
67                "Failed to read Vite manifest: {$this->manifestPath}"
68            );
69        }
70
71        $manifest = json_decode($contents, true);
72        if (json_last_error() !== JSON_ERROR_NONE) {
73            throw new ViteManifestException(
74                "Invalid JSON in Vite manifest: " . json_last_error_msg()
75            );
76        }
77
78        $this->manifest = $manifest;
79        return $this->manifest;
80    }
81
82    /**
83     * Check if manifest exists and is readable
84     *
85     * @return bool True if manifest exists and is valid
86     */
87    public function exists(): bool
88    {
89        try {
90            $this->load();
91            return true;
92        } catch (ViteManifestException $e) {
93            return false;
94        }
95    }
96
97    /**
98     * Get entry point data from manifest
99     *
100     * @param string $entry Entry name (e.g., 'workspace', 'admin')
101     * @return array Entry data with 'file', 'css', 'imports' keys
102     * @throws ViteManifestException If entry not found
103     */
104    public function getEntry(string $entry): array
105    {
106        $manifest = $this->load();
107
108        // Vite manifest keys are the input file paths (e.g., "resources/js/workspace.js")
109        $entryKey = "resources/js/{$entry}.js";
110
111        if (!isset($manifest[$entryKey])) {
112            // Also try just the entry name (for direct references)
113            if (!isset($manifest[$entry])) {
114                throw new ViteManifestException(
115                    "Entry '{$entry}' not found in Vite manifest. " .
116                    "Available entries: " . implode(', ', $this->getEntryNames())
117                );
118            }
119            return $manifest[$entry];
120        }
121
122        return $manifest[$entryKey];
123    }
124
125    /**
126     * Get the JavaScript file URL for an entry
127     *
128     * @param string $entry Entry name
129     * @return string Full URL to the JS file
130     */
131    public function getJsUrl(string $entry): string
132    {
133        $entryData = $this->getEntry($entry);
134        return $this->baseUrl . $entryData['file'];
135    }
136
137    /**
138     * Get CSS file URLs for an entry
139     *
140     * @param string $entry Entry name
141     * @return array Array of CSS file URLs
142     */
143    public function getCssUrls(string $entry): array
144    {
145        $entryData = $this->getEntry($entry);
146
147        if (!isset($entryData['css']) || empty($entryData['css'])) {
148            return [];
149        }
150
151        return array_map(
152            fn(string $file) => $this->baseUrl . $file,
153            $entryData['css']
154        );
155    }
156
157    /**
158     * Get preload URLs for an entry (imported chunks)
159     *
160     * @param string $entry Entry name
161     * @return array Array of preload URLs
162     */
163    public function getPreloadUrls(string $entry): array
164    {
165        $entryData = $this->getEntry($entry);
166
167        if (!isset($entryData['imports']) || empty($entryData['imports'])) {
168            return [];
169        }
170
171        $manifest = $this->load();
172        $preloads = [];
173
174        foreach ($entryData['imports'] as $import) {
175            if (isset($manifest[$import])) {
176                $preloads[] = $this->baseUrl . $manifest[$import]['file'];
177            }
178        }
179
180        return $preloads;
181    }
182
183    /**
184     * Get list of available entry names
185     *
186     * @return array Entry names
187     */
188    public function getEntryNames(): array
189    {
190        $manifest = $this->load();
191        $entries = [];
192
193        foreach (array_keys($manifest) as $key) {
194            // Extract entry name from "resources/js/entryname.js"
195            if (preg_match('#^resources/js/([^/]+)\.js$#', $key, $matches)) {
196                $entries[] = $matches[1];
197            }
198        }
199
200        return $entries;
201    }
202
203    /**
204     * Check if an entry exists in the manifest
205     *
206     * @param string $entry Entry name
207     * @return bool True if entry exists
208     */
209    public function hasEntry(string $entry): bool
210    {
211        try {
212            $this->getEntry($entry);
213            return true;
214        } catch (ViteManifestException $e) {
215            return false;
216        }
217    }
218
219    /**
220     * Clear cached manifest (useful for testing)
221     */
222    public function clearCache(): void
223    {
224        $this->manifest = null;
225    }
226}