8000 [AssetMapper] Fix: also download files referenced by url() in CSS · symfony/symfony@ca90ed8 · GitHub
[go: up one dir, main page]

Skip to content

Commit ca90ed8

Browse files
weaverryanfabpot
authored andcommitted
[AssetMapper] Fix: also download files referenced by url() in CSS
1 parent b85a083 commit ca90ed8

File tree

8 files changed

+305
-46
lines changed

8 files changed

+305
-46
lines changed

src/Symfony/Component/AssetMapper/Command/ImportMapInstallCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6464
}
6565

6666
$io->success(sprintf(
67-
'Downloaded %d asset%s into %s.',
67+
'Downloaded %d package%s into %s.',
6868
\count($downloadedPackages),
6969
1 === \count($downloadedPackages) ? '' : 's',
7070
str_replace($this->projectDir.'/', '', $this->packageDownloader->getVendorDir()),

src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public function downloadPackages(callable $progressCallback = null): array
5252
isset($installed[$entry->importName])
5353
&& $installed[$entry->importName]['version'] === $entry->version
5454
&& $this->remotePackageStorage->isDownloaded($entry)
55+
&& $this->areAllExtraFilesDownloaded($entry, $installed[$entry->importName]['extraFiles'])
5556
) {
5657
$newInstalled[$entry->importName] = $installed[$entry->importName];
5758
continue;
@@ -72,9 +73,14 @@ public function downloadPackages(callable $progressCallback = null): array
7273
}
7374

7475
$this->remotePackageStorage->save($entry, $contents[$package]['content']);
76+
foreach ($contents[$package]['extraFiles'] as $extraFilename => $extraFileContents) {
77+
$this->remotePackageStorage->saveExtraFile($entry, $extraFilename, $extraFileContents);
78+
}
79+
7580
$newInstalled[$package] = [
7681
'version' => $entry->version,
7782
'dependencies' => $contents[$package]['dependencies'] ?? [],
83+
'extraFiles' => array_keys($contents[$pac 6293 kage]['extraFiles']),
7884
];
7985

8086
$downloadedPackages[] = $package;
@@ -109,7 +115,7 @@ public function getVendorDir(): string
109115
}
110116

111117
/**
112-
* @return array<string, array{path: string, version: string, dependencies: array<string, string>}>
118+
* @return array<string, array{path: string, version: string, dependencies: array<string, string>, extraFiles: array<string, string>}>
113119
*/
114120
private function loadInstalled(): array
115121
{
@@ -128,6 +134,10 @@ private function loadInstalled(): array
128134
if (!isset($data['dependencies'])) {
129135
throw new \LogicException(sprintf('The package "%s" is missing its dependencies.', $package));
130136
}
137+
138+
if (!isset($data['extraFiles'])) {
139+
$installed[$package]['extraFiles'] = [];
140+
}
131141
}
132142

133143
return $this->installed = $installed;
@@ -138,4 +148,15 @@ private function saveInstalled(array $installed): void
138148
$this->installed = $installed;
139149
file_put_contents($this->remotePackageStorage->getStorageDir().'/installed.php', sprintf('<?php return %s;', var_export($installed, true)));
140150
}
151+
152+
private function areAllExtraFilesDownloaded(ImportMapEntry $entry, array $extraFilenames): bool
153+
{
154+
foreach ($extraFilenames as $extraFilename) {
155+
if (!$this->remotePackageStorage->isExtraFileDownloaded($entry, $extraFilename)) {
156+
return false;
157+
}
158+
}
159+
160+
return true;
161+
}
141162
}

src/Symfony/Component/AssetMapper/ImportMap/RemotePackageStorage.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ public function isDownloaded(ImportMapEntry $entry): bool
3434
return is_file($this->getDownloadPath($entry->packageModuleSpecifier, $entry->type));
3535
}
3636

37+
public function isExtraFileDownloaded(ImportMapEntry $entry, string $extraFilename): bool
38+
{
39+
if (!$entry->isRemotePackage()) {
40+
throw new \InvalidArgumentException(sprintf('The entry "%s" is not a remote package.', $entry->importName));
41+
}
42+
43+
return is_file($this->getExtraFileDownloadPath($entry, $extraFilename));
44+
}
45+
3746
public function save(ImportMapEntry $entry, string $contents): void
3847
{
3948
if (!$entry->isRemotePackage()) {
@@ -46,6 +55,18 @@ public function save(ImportMapEntry $entry, string $contents): void
4655
file_put_contents($vendorPath, $contents);
4756
}
4857

58+
public function saveExtraFile(ImportMapEntry $entry, string $extraFilename, string $contents): void
59+
{
60+
if (!$entry->isRemotePackage()) {
61+
throw new \InvalidArgumentException(sprintf('The entry "%s" is not a remote package.', $entry->importName));
62+
}
63+
64+
$vendorPath = $this->getExtraFileDownloadPath($entry, $extraFilename);
65+
66+
@mkdir(\dirname($vendorPath), 0777, true);
67+
file_put_contents($vendorPath, $contents);
68+
}
69+
4970
/**
5071
* The local file path where a downloaded package should be stored.
5172
*/
@@ -68,4 +89,9 @@ public function getDownloadPath(string $packageModuleSpecifier, ImportMapType $i
6889

6990
return $this->vendorDir.'/'.$filename;
7091
}
92+
93+
private function getExtraFileDownloadPath(ImportMapEntry $entry, string $extraFilename): string
94+
{
95+
return $this->vendorDir.'/'.$entry->getPackageName().'/'.ltrim($extraFilename, '/');
96+
}
7197
}

src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
namespace Symfony\Component\AssetMapper\ImportMap\Resolver;
1313

14+
use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler;
1415
use Symfony\Component\AssetMapper\Exception\RuntimeException;
1516
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
1617
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
1718
use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions;
19+
use Symfony\Component\Filesystem\Path;
1820
use Symfony\Component\HttpClient\HttpClient;
1921
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
2022
use 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);

src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolverInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function resolvePackages(array $packagesToRequire): array;
3737
*
3838
* @param array<string, ImportMapEntry> $importMapEntries
3939
*
40-
* @return array<string, array{content: string, dependencies: string[]}>
40+
* @return array<string, array{content: string, dependencies: string[], extraFiles: array<string, string>}>
4141
*/
4242
public function downloadPackages(array $importMapEntries, callable $progressCallback = null): array;
4343
}

0 commit comments

Comments
 (0)
0