• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / assets / 20959429705

13 Jan 2026 01:58PM UTC coverage: 94.048%. Remained the same
20959429705

push

github

dg
added CLAUDE.md

474 of 504 relevant lines covered (94.05%)

0.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.91
/src/Assets/ViteMapper.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Nette\Assets;
6

7
use Nette\Utils\FileSystem;
8
use Nette\Utils\Json;
9
use function array_filter, array_values, is_array, preg_match, str_starts_with;
10

11

12
/**
13
 * Maps asset references to Vite-generated files using a Vite manifest.json.
14
 * Supports both development mode (Vite dev server) and production mode.
15
 */
16
class ViteMapper implements Mapper
17
{
18
        /** @var array<string, array{file: string, isEntry?: bool, isDynamicEntry?: bool, css?: list<string>, imports?: list<string>}> */
19
        private array $chunks;
20

21
        /** @var array<string, ?array<string, Asset>> cached dependencies keyed by chunkId */
22
        private array $dependencies = [];
23

24

25
        public function __construct(
1✔
26
                private readonly string $baseUrl,
27
                private readonly string $basePath,
28
                private readonly ?string $manifestPath = null,
29
                private readonly ?string $devServer = null,
30
                private readonly ?Mapper $publicMapper = null,
31
        ) {
32
                if ($devServer !== null && !str_starts_with($devServer, 'http')) {
1✔
33
                        throw new \InvalidArgumentException("Vite devServer must be absolute URL, '$devServer' given");
×
34
                }
35
        }
1✔
36

37

38
        /**
39
         * Retrieves an Asset for a given Vite entry point.
40
         * @param  array<empty, empty>  $options not used, must be empty
41
         * @throws AssetNotFoundException when the asset cannot be found in the manifest
42
         */
43
        public function getAsset(string $reference, array $options = []): Asset
1✔
44
        {
45
                Helpers::checkOptions($options);
1✔
46

47
                if ($this->devServer) {
1✔
48
                        return $this->createDevelopmentAsset($reference);
1✔
49
                }
50

51
                $this->chunks ??= $this->readChunks();
1✔
52

53
                if (isset($this->chunks[$reference])) {
1✔
54
                        return $this->createProductionAsset($reference);
1✔
55

56
                } elseif ($this->publicMapper) {
1✔
57
                        return $this->publicMapper->getAsset($reference);
1✔
58

59
                } else {
60
                        throw new AssetNotFoundException("File '$reference' not found in Vite manifest");
1✔
61
                }
62
        }
63

64

65
        private function createProductionAsset(string $reference): Asset
1✔
66
        {
67
                $chunk = $this->chunks[$reference];
1✔
68
                $entry = isset($chunk['isEntry']) || isset($chunk['isDynamicEntry']);
1✔
69
                if (str_starts_with($reference, '_') && !$entry) {
1✔
70
                        throw new AssetNotFoundException("Cannot directly access internal chunk '$reference'");
1✔
71
                }
72

73
                $dependencies = $this->collectDependencies($reference);
1✔
74
                unset($dependencies[$chunk['file']]);
1✔
75

76
                return $dependencies
1✔
77
                        ? new EntryAsset(
1✔
78
                                url: $this->baseUrl . '/' . $chunk['file'],
1✔
79
                                file: $this->basePath . '/' . $chunk['file'],
1✔
80
                                imports: array_values(array_filter($dependencies, fn($asset) => $asset instanceof StyleAsset)),
1✔
81
                                preloads: array_values(array_filter($dependencies, fn($asset) => $asset instanceof ScriptAsset)),
1✔
82
                                crossorigin: true,
1✔
83
                        )
84
                        : Helpers::createAssetFromUrl(
1✔
85
                                $this->baseUrl . '/' . $chunk['file'],
1✔
86
                                $this->basePath . '/' . $chunk['file'],
1✔
87
                                ['crossorigin' => true],
1✔
88
                        );
89
        }
90

91

92
        private function createDevelopmentAsset(string $reference): Asset
1✔
93
        {
94
                $url = $this->devServer . '/' . $reference;
1✔
95
                return match (1) {
96
                        preg_match('~\.(jsx?|mjs|tsx?)$~i', $reference) => new EntryAsset(
1✔
97
                                url: $url,
1✔
98
                                imports: [new ScriptAsset($this->devServer . '/@vite/client', type: 'module')],
1✔
99
                        ),
100
                        preg_match('~\.(sass|scss)$~i', $reference) => new StyleAsset($url),
1✔
101
                        default => Helpers::createAssetFromUrl($url),
1✔
102
                };
103
        }
104

105

106
        /**
107
         * Recursively collects all imports (including nested) from a chunk.
108
         * @return array<string, Asset>
109
         */
110
        private function collectDependencies(string $chunkId): array
1✔
111
        {
112
                $deps = &$this->dependencies[$chunkId];
1✔
113
                if ($deps === null) {
1✔
114
                        $deps = [];
1✔
115
                        $chunk = $this->chunks[$chunkId] ?? [];
1✔
116
                        foreach ($chunk['css'] ?? [] as $file) {
1✔
117
                                $deps[$file] = Helpers::createAssetFromUrl(
1✔
118
                                        $this->baseUrl . '/' . $file,
1✔
119
                                        $this->basePath . '/' . $file,
1✔
120
                                        ['crossorigin' => true],
1✔
121
                                );
122
                        }
123
                        foreach ($chunk['imports'] ?? [] as $id) {
1✔
124
                                $file = $this->chunks[$id]['file'];
1✔
125
                                $deps[$file] = Helpers::createAssetFromUrl(
1✔
126
                                        $this->baseUrl . '/' . $file,
1✔
127
                                        $this->basePath . '/' . $file,
1✔
128
                                        ['type' => 'module', 'crossorigin' => true],
1✔
129
                                );
130
                                $deps += $this->collectDependencies($id);
1✔
131
                        }
132
                }
133
                return $deps;
1✔
134
        }
135

136

137
        /**
138
         * @return array<string, array{file: string, isEntry?: bool, isDynamicEntry?: bool, css?: list<string>, imports?: list<string>}>
139
         */
140
        private function readChunks(): array
141
        {
142
                $path = $this->manifestPath ?? $this->basePath . '/.vite/manifest.json';
1✔
143
                try {
144
                        $res = Json::decode(FileSystem::read($path), forceArrays: true);
1✔
145
                } catch (\Throwable $e) {
×
146
                        throw new \RuntimeException("Failed to read Vite manifest from '$path'. Did you run 'npm run build'?", 0, $e);
×
147
                }
148
                if (!is_array($res)) {
1✔
149
                        throw new \RuntimeException("Invalid Vite manifest format in '$path'");
×
150
                }
151
                return $res;
1✔
152
        }
153

154

155
        /**
156
         * Returns the base URL for this mapper.
157
         */
158
        public function getBaseUrl(): string
159
        {
160
                return $this->baseUrl;
×
161
        }
162

163

164
        /**
165
         * Returns the base path for this mapper.
166
         */
167
        public function getBasePath(): string
168
        {
169
                return $this->basePath;
×
170
        }
171
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc