1111
1212namespace Symfony \Component \AssetMapper \ImportMap \Resolver ;
1313
14+ use Symfony \Component \AssetMapper \Compiler \CssAssetUrlCompiler ;
1415use Symfony \Component \AssetMapper \Exception \RuntimeException ;
1516use Symfony \Component \AssetMapper \ImportMap \ImportMapEntry ;
1617use Symfony \Component \AssetMapper \ImportMap \ImportMapType ;
1718use Symfony \Component \AssetMapper \ImportMap \PackageRequireOptions ;
19+ use Symfony \Component \Filesystem \Path ;
1820use Symfony \Component \HttpClient \HttpClient ;
1921use Symfony \Contracts \HttpClient \Exception \HttpExceptionInterface ;
2022use Symfony \Contracts \HttpClient \HttpClientInterface ;
@@ -157,12 +159,11 @@ public function resolvePackages(array $packagesToRequire): array
157159 /**
158160 * @param ImportMapEntry[] $importMapEntries
159161 *
160- * @return array<string, array{content: string, dependencies: string[]}>
162+ * @return array<string, array{content: string, dependencies: string[], extraFiles: array<string, string> }>
161163 */
162164 public function downloadPackages (array $ importMapEntries , callable $ progressCallback = null ): array
163165 {
164166 $ responses = [];
165-
166167 foreach ($ importMapEntries as $ package => $ entry ) {
167168 if (!$ entry ->isRemotePackage ()) {
168169 throw new \InvalidArgumentException (sprintf ('The entry "%s" is not a remote package. ' , $ entry ->importName ));
@@ -171,12 +172,13 @@ public function downloadPackages(array $importMapEntries, callable $progressCall
171172 $ pattern = ImportMapType::CSS === $ entry ->type ? self ::URL_PATTERN_DIST_CSS : self ::URL_PATTERN_DIST ;
172173 $ url = sprintf ($ pattern , $ entry ->getPackageName (), $ entry ->version , $ entry ->getPackagePathString ());
173174
174- $ responses [$ package ] = $ this ->httpClient ->request ('GET ' , $ url );
175+ $ responses [$ package ] = [ $ this ->httpClient ->request ('GET ' , $ url ), $ entry ] ;
175176 }
176177
177178 $ errors = [];
178179 $ contents = [];
179- foreach ($ responses as $ package => $ response ) {
180+ $ extraFileResponses = [];
181+ foreach ($ responses as $ package => [$ response , $ entry ]) {
180182 if (200 !== $ response ->getStatusCode ()) {
181183 $ errors [] = [$ package , $ response ];
182184 continue ;
@@ -187,10 +189,21 @@ public function downloadPackages(array $importMapEntries, callable $progressCall
187189 }
188190
189191 $ dependencies = [];
192+ $ extraFiles = [];
193+ /* @var ImportMapEntry $entry */
190194 $ contents [$ package ] = [
191- 'content ' => $ this ->makeImportsBare ($ response ->getContent (), $ dependencies ),
195+ 'content ' => $ this ->makeImportsBare ($ response ->getContent (), $ dependencies, $ extraFiles , $ entry -> type , $ entry -> getPackagePathString () ),
192196 'dependencies ' => $ dependencies ,
197+ 'extraFiles ' => [],
193198 ];
199+
200+ if (0 !== \count ($ extraFiles )) {
201+ $ extraFileResponses [$ package ] = [];
202+ foreach ($ extraFiles as $ extraFile ) {
203+ $ extraFileResponses [$ package ][] = [$ this ->httpClient ->request ('GET ' , sprintf (self ::URL_PATTERN_DIST_CSS , $ entry ->getPackageName (), $ entry ->version , $ extraFile )), $ extraFile , $ entry ->getPackageName (), $ entry ->version ];
204+ }
205+ }
206+
194207 if ($ progressCallback ) {
195208 $ progressCallback ($ package , 'finished ' , $ response , \count ($ responses ));
196209 }
@@ -205,6 +218,47 @@ public function downloadPackages(array $importMapEntries, callable $progressCall
205218 throw new RuntimeException (sprintf ('Error %d downloading packages from jsDelivr for "%s". Check your package names. Response: ' , $ response ->getStatusCode (), $ packages ).$ response ->getContent (false ), 0 , $ e );
206219 }
207220
221+ $ extraFileErrors = [];
222+ download_extra_files:
223+ $ packageFileResponses = $ extraFileResponses ;
224+ $ extraFileResponses = [];
225+ foreach ($ packageFileResponses as $ package => $ responses ) {
226+ foreach ($ responses as [$ response , $ extraFile , $ packageName , $ version ]) {
227+ if (200 !== $ response ->getStatusCode ()) {
228+ $ extraFileErrors [] = [$ package , $ response ];
229+ continue ;
230+ }
231+
232+ $ extraFiles = [];
233+
234+ $ content = $ response ->getContent ();
235+ if (str_ends_with ($ extraFile , '.css ' )) {
236+ $ content = $ this ->makeImportsBare ($ content , $ dependencies , $ extraFiles , ImportMapType::CSS , $ extraFile );
237+ }
238+ $ contents [$ package ]['extraFiles ' ][$ extraFile ] = $ content ;
239+
240+ if (0 !== \count ($ extraFiles )) {
241+ $ extraFileResponses [$ package ] = [];
242+ foreach ($ extraFiles as $ newExtraFile ) {
243+ $ extraFileResponses [$ package ][] = [$ this ->httpClient ->request ('GET ' , sprintf (self ::URL_PATTERN_DIST_CSS , $ packageName , $ version , $ newExtraFile )), $ newExtraFile , $ packageName , $ version ];
244+ }
245+ }
246+ }
247+ }
248+
249+ if ($ extraFileResponses ) {
250+ goto download_extra_files;
251+ }
252+
253+ try {
254+ ($ extraFileErrors [0 ][1 ] ?? null )?->getHeaders();
255+ } catch (HttpExceptionInterface $ e ) {
256+ $ response = $ e ->getResponse ();
257+ $ packages = implode ('", " ' , array_column ($ extraFileErrors , 0 ));
258+
259+ throw new RuntimeException (sprintf ('Error %d downloading extra imported files from jsDelivr for "%s". Response: ' , $ response ->getStatusCode (), $ packages ).$ response ->getContent (false ), 0 , $ e );
260+ }
261+
208262 return $ contents ;
209263 }
210264
@@ -237,20 +291,37 @@ private function fetchPackageRequirementsFromImports(string $content): array
237291 *
238292 * Replaces those with normal import "package/name" statements.
239293 */
240- private function makeImportsBare (string $ content , array &$ dependencies ): string
294+ private function makeImportsBare (string $ content , array &$ dependencies, array & $ extraFiles , ImportMapType $ type , string $ sourceFilePath ): string
241295 {
242- $ content = preg_replace_callback (self ::IMPORT_REGEX , function ($ matches ) use (&$ dependencies ) {
243- $ packageName = $ matches [2 ].$ matches [4 ]; // add the path if any
244- $ dependencies [] = $ packageName ;
245-
246- // replace the "/npm/package@version/+esm" with "package@version"
247- return str_replace ($ matches [1 ], sprintf ('"%s" ' , $ packageName ), $ matches [0 ]);
248- }, $ content );
249-
250- // source maps are not also downloaded - so remove the sourceMappingURL
251- // remove the final one only (in case sourceMappingURL is used in the code)
252- if (false !== $ lastPos = strrpos ($ content , '//# sourceMappingURL= ' )) {
253- $ content = substr ($ content , 0 , $ lastPos ).preg_replace ('{//# sourceMappingURL=.*$}m ' , '' , substr ($ content , $ lastPos ));
296+ if (ImportMapType::JS === $ type ) {
297+ $ content = preg_replace_callback (self ::IMPORT_REGEX , function ($ matches ) use (&$ dependencies ) {
298+ $ packageName = $ matches [2 ].$ matches [4 ]; // add the path if any
299+ $ dependencies [] = $ packageName ;
300+
301+ // replace the "/npm/package@version/+esm" with "package@version"
302+ return str_replace ($ matches [1 ], sprintf ('"%s" ' , $ packageName ), $ matches [0 ]);
303+ }, $ content );
304+
305+ // source maps are not also downloaded - so remove the sourceMappingURL
306+ // remove the final one only (in case sourceMappingURL is used in the code)
307+ if (false !== $ lastPos = strrpos ($ content , '//# sourceMappingURL= ' )) {
308+ $ content = substr ($ content , 0 , $ lastPos ).preg_replace ('{//# sourceMappingURL=.*$}m ' , '' , substr ($ content , $ lastPos ));
309+ }
310+
311+ return $ content ;
312+ }
313+
314+ preg_match_all (CssAssetUrlCompiler::ASSET_URL_PATTERN , $ content , $ matches );
315+ foreach ($ matches [1 ] as $ path ) {
316+ if (str_starts_with ($ path , 'data: ' )) {
317+ continue ;
318+ }
319+
320+ if (str_starts_with ($ path , 'http:// ' ) || str_starts_with ($ path , 'https:// ' )) {
321+ continue ;
322+ }
323+
324+ $ extraFiles [] = Path::join (\dirname ($ sourceFilePath ), $ path );
254325 }
255326
256327 return preg_replace ('{/\*# sourceMappingURL=[^ ]*+ \*/} ' , '' , $ content );
0 commit comments