8000 [AssetMapper] Adding support for type: css in importmap · symfony/symfony@f75aad9 · GitHub
[go: up one dir, main page]

Skip to content

Commit f75aad9

Browse files
committed
[AssetMapper] Adding support for type: css in importmap
1 parent 9b2a4db commit f75aad9

24 files changed

+358
-52
lines changed

src/Symfony/Bridge/Twig/Extension/ImportMapExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public function getFunctions(): array
2323
{
2424
return [
2525
new TwigFunction('importmap', [ImportMapRuntime::class, 'importmap'], ['is_safe' => ['html']]),
26+
new TwigFunction('importmap_link_tags', [ImportMapRuntime::class, 'linkTags'], ['is_safe' => ['html']]),
2627
];
2728
}
2829
}

src/Symfony/Bridge/Twig/Extension/ImportMapRuntime.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@ public function importmap(?string $entryPoint = 'app', array $attributes = []):
2626
{
2727
return $this->importMapRenderer->render($entryPoint, $attributes);
2828
}
29+
30+
public function linkTags(): string
31+
{
32+
return $this->importMapRenderer->renderLinkTags();
33+
}
2934
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
9393
}
9494
$this->filesystem->dumpFile(
9595
$importMapPreloadPath,
96-
json_encode($this->importMapManager->getModulesToPreload(), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)
96+
json_encode([
97+
'modules' => $this->importMapManager->getModulesToPreload(),
98+
'linkTags' => $this->importMapManager->getLinkTags(),
99+
], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)
97100
);
98101
$io->comment(sprintf('Import map written to <info>%s</info> and <info>%s</info> for quick importmap dumping onto the page.', $this->shortenPath($importMapPath), $this->shortenPath($importMapPreloadPath)));
99102

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
126126
);
127127
}
128128

129-
if ($input->getOption('download')) {
130-
$io->warning(sprintf('The --download option is experimental. It should work well with the default %s provider but check your browser console for 404 errors.', ImportMapManager::PROVIDER_JSDELIVR_ESM));
131-
}
132-
133129
$newPackages = $this->importMapManager->require($packages);
134130
if (1 === \count($newPackages)) {
135131
$newPackage = $newPackages[0];
@@ -151,7 +147,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
151147
$message .= '.';
152148
} else {
153149
$names = array_map(fn (ImportMapEntry $package) => $package->importName, $newPackages);
154-
$message = sprintf('%d new packages (%s) added to the importmap.php!', \count($newPackages), implode(', ', $names));
150+
$message = sprintf('%d new items (%s) added to the importmap.php!', \count($newPackages), implode(', ', $names));
155151
}
156152

157153
$messages = [$message];

src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,16 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
6565
return $matches[0];
6666
}
6767

68-
if ($this->supports($dependentAsset)) {
69-
// If we found the path and it's a JavaScript file, list it as a dependency.
70-
// This will cause the asset to be included in the importmap.
71-
$isLazy = str_contains($matches[0], 'import(');
68+
// We found the path! List it as a dependency.
69+
// This will cause the asset to be included in the importmap.
70+
$isLazy = str_contains($matches[0], 'import(');
7271

73-
$asset->addDependency(new AssetDependency($dependentAsset, $isLazy, false));
72+
$asset->addDependency(new AssetDependency($dependentAsset, $isLazy, false));
7473

75-
$relativeImportPath = $this->createRelativePath($asset->publicPathWithoutDigest, $dependentAsset->publicPathWithoutDigest);
76-
67F4 $relativeImportPath = $this->makeRelativeForJavaScript($relativeImportPath);
74+
$relativeImportPath = $this->createRelativePath($asset->publicPathWithoutDigest, $dependentAsset->publicPathWithoutDigest);
75+
$relativeImportPath = $this->makeRelativeForJavaScript($relativeImportPath);
7776

78-
return str_replace($matches[1], $relativeImportPath, $matches[0]);
79-
}
80-
81-
return $matches[0];
77+
return str_replace($matches[1], $relativeImportPath, $matches[0]);
8278
}, $content);
8379
}
8480

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
*/
1919
final class ImportMapEntry
2020
{
21+
public const TYPE_JS = 'js';
22+
public const TYPE_CSS = 'css';
23+
2124
public function __construct(
2225
/**
2326
* The logical path to this asset if local or downloaded.
@@ -27,6 +30,12 @@ public function __construct(
2730
public readonly ?string $url = null,
2831
public readonly bool $isDownloaded = false,
2932
public readonly bool $preload = false,
33+
public readonly string $type = self::TYPE_JS,
3034
) {
3135
}
36+
37+
public static function getValidTypes(): array
38+
{
39+
return [self::TYPE_JS, self::TYPE_CSS];
40+
}
3241
}

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

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class ImportMapManager
5454

5555
private array $importMapEntries;
5656
private array $modulesToPreload;
57+
private array $linkTags;
5758
private string $json;
5859

5960
public function __construct(
@@ -72,6 +73,13 @@ public function getModulesToPreload(): array
7273
return $this->modulesToPreload;
7374
}
7475

76+
public function getLinkTags(): array
77+
{
78+
$this->buildImportMapJson();
79+
80+
return $this->linkTags;
81+
}
82+
7583
public function getImportMapJson(): string
7684
{
7785
$this->buildImportMapJson();
@@ -168,15 +176,18 @@ private function buildImportMapJson(): void
168176
$dumpedModulePreloadPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::IMPORT_MAP_PRELOAD_FILE_NAME;
169177
if (is_file($dumpedImportMapPath) && is_file($dumpedModulePreloadPath)) {
170178
$this->json = file_get_contents($dumpedImportMapPath);
171-
$this->modulesToPreload = json_decode(file_get_contents($dumpedModulePreloadPath), true, 512, \JSON_THROW_ON_ERROR);
179+
$preload = json_decode(file_get_contents($dumpedModulePreloadPath), true, 512, \JSON_THROW_ON_ERROR);
180+
$this->linkTags = $preload['linkTags'] ?? [];
181+
$this->modulesToPreload = $preload['modules'] ?? [];
172182

173183
return;
174184
}
175185

176186
$entries = $this->loadImportMapEntries();
177187
$this->modulesToPreload = [];
188+
$this->linkTags = [];
178189

179-
$imports = $this->convertEntriesToImports($entries);
190+
$imports = $this->convertEntriesToImports($entries, true);
180191

181192
$importmap['imports'] = $imports;
182193

@@ -280,12 +291,13 @@ private function requirePackages(array $packagesToRequire, array &$importMapEntr
280291
foreach ($resolvedPackages as $resolvedPackage) {
281292
$importName = $resolvedPackage->requireOptions->importName ?: $resolvedPackage->requireOptions->packageName;
282293
$path = null;
294+
$type = str_ends_with($resolvedPackage->url, '.css') ? ImportMapEntry::TYPE_CSS : ImportMapEntry::TYPE_JS;
283295
if ($resolvedPackage->requireOptions->download) {
284296
if (null === $resolvedPackage->content) {
285297
throw new \LogicException(sprintf('The contents of package "%s" were not downloaded.', $resolvedPackage->requireOptions->packageName));
286298
}
287299

288-
$path = $this->downloadPackage($importName, $resolvedPackage->content);
300+
$path = $this->downloadPackage($importName, $resolvedPackage->content, $type);
289301
}
290302

291303
$newEntry = new ImportMapEntry(
@@ -294,6 +306,7 @@ private function requirePackages(array $packagesToRequire, array &$importMapEntr
294306
$resolvedPackage->url,
295307
$resolvedPackage->requireOptions->download,
296308
$resolvedPackage->requireOptions->preload,
309+
$type
297310
);
298311
$importMapEntries[$importName] = $newEntry;
299312
$addedEntries[] = $newEntry;
@@ -329,12 +342,23 @@ private function loadImportMapEntries(): array
329342

330343
$entries = [];
331344
foreach ($importMapConfig ?? [] as $importName => $data) {
345+
$validKeys = ['path', 'url', 'downloaded_to', 'type', 'preload'];
346+
if ($invalidKeys = array_diff(array_keys($data), $validKeys)) {
347+
throw new \InvalidArgumentException(sprintf('The following keys are not valid for the importmap entry "%s": "%s". Valid keys are: "%s".', $importName, implode('", "', $invalidKeys), implode('", "', $validKeys)));
348+
}
349+
350+
$type = $data['type'] ?? ImportMapEntry::TYPE_JS;
351+
if (!in_array($type, ImportMapEntry::getValidTypes(), true)) {
352+
throw new \InvalidArgumentException(sprintf('The "type" for import "%s" must be one of %s, "%s" given.', $importName, implode(', ', ImportMapEntry::getValidTypes()), $type));
353+
}
354+
332355
$entries[$importName] = new ImportMapEntry(
333356
$importName,
334357
path: $data['path'] ?? $data['downloaded_to'] ?? null,
335358
url: $data['url'] ?? null,
336359
isDownloaded: isset($data['downloaded_to']),
337360
preload: $data['preload'] ?? false,
361+
type: $type,
338362
);
339363
}
340364

@@ -348,6 +372,7 @@ private function writeImportMapConfig(array $entries): void
348372
{
349373
$this->importMapEntries = $entries;
350374
unset($this->modulesToPreload);
375+
unset($this->linkTags);
351376
unset($this->json);
352377

353378
$importMapConfig = [];
@@ -370,6 +395,9 @@ private function writeImportMapConfig(array $entries): void
370395
if ($entry->preload) {
371396
$config['preload'] = $entry->preload;
372397
}
398+
if ($entry->type !== ImportMapEntry::TYPE_JS) {
399+
$config['type'] = $entry->type;
400+
}
373401
$importMapConfig[$entry->importName] = $config;
374402
}
375403

@@ -378,13 +406,14 @@ private function writeImportMapConfig(array $entries): void
378406
<?php
379407
380408
/**
381-
* Returns the import map for this application.
409+
* Returns the importmap for this application.
382410
*
383411
* - "path" is a path inside the asset mapper system. Use the
384412
* "debug:asset-map" command to see the full list of paths.
385413
*
386-
* - "preload" set to true for any modules that are loaded on the initial
387-
* page load to help the browser download them earlier.
414+
* - "preload"
415+
* For JavaScript: adds a modulepreload link to help the browser download it earlier.
416+
* For CSS: adds a <link rel="stylesheet" tag for the CSS.
388417
*
389418
* The "importmap:require" command can be used to add new entries to this file.
390419
*
@@ -396,9 +425,14 @@ private function writeImportMapConfig(array $entries): void
396425
}
397426

398427
/**
428+
* Returns an array of "import name" => "URL".
429+
*
430+
* This also records the modulesToPreload & linkTags.
431+
*
399432
* @param ImportMapEntry[] $entries
433+
* @return array<string, string>
400434
*/
401-
private function convertEntriesToImports(array $entries): array
435+
private function convertEntriesToImports(array $entries, bool $isTopLevel): array
402436
{
403437
$imports = [];
404438
foreach ($entries as $entryOptions) {
@@ -425,28 +459,55 @@ private function convertEntriesToImports(array $entries): array
425459
throw new \InvalidArgumentException(sprintf('The package "%s" mentioned in "%s" must have a "path" or "url" key.', $entryOptions->importName, basename($this->importMapConfigPath)));
426460
}
427461

428-
$imports[$entryOptions->importName] = $path;
462+
if (ImportMapEntry::TYPE_CSS === $entryOptions->type) {
463+
if ($entryOptions->preload) {
464+
// importmap is a noop because this will be rendered as a link tag
465+
$this->linkTags[] = $path;
466+
// As an optimization, CSS files that live directly in importmap.php
467+
// and which are preload, do not need to be added to the importmap:
468+
// these are only meant to be rendered as link tags.
469+
if (!$isTopLevel) {
470+
$imports[$entryOptions->importName] = 'data:application/javascript,';
471+
}
472+
} else {
473+
// if this is not preloaded, we need to write a link tag when it is loaded
474+
$imports[$entryOptions->importName] = 'data:application/javascript,'.rawurlencode(sprintf('const d=document,l=d.createElement("link");l.href="%s",l.rel="stylesheet",(d.head||d.getElementsByTagName("head")[0]).appendChild(l)', $path));
475+
}
476+
477+
// do not try to add CSS dependencies to the importmap
478+
continue;
479+
}
429480

481+
// type: js
430482
if ($entryOptions->preload ?? false) {
431483
$this->modulesToPreload[] = $path;
432484
}
485+
$imports[$entryOptions->importName] = $path;
433486

434487
$dependencyImportMapEntries = array_map(function (AssetDependency $dependency) use ($entryOptions) {
435488
return new ImportMapEntry(
436489
$dependency->asset->publicPathWithoutDigest,
437490
$dependency->asset->logicalPath,
491+
// if parent is preload & dependency is not laz 1E0A y, then preload
438492
preload: $entryOptions->preload && !$dependency->isLazy,
493+
type: $dependency->asset->publicExtension === 'css' ? ImportMapEntry::TYPE_CSS : ImportMapEntry::TYPE_JS,
439494
);
440495
}, $dependencies);
441-
$imports = array_merge($imports, $this->convertEntriesToImports($dependencyImportMapEntries));
496+
497+
// Add dependencies to the import maps array.
498+
$imports = array_merge($imports, $this->convertEntriesToImports($dependencyImportMapEntries, false));
442499
}
443500

444501
return $imports;
445502
}
446503

447-
private function downloadPackage(string $packageName, string $packageContents): string
504+
private function downloadPackage(string $packageName, string $packageContents, string $importMapType): string
448505
{
449-
$vendorPath = $this->vendorDir.'/'.$packageName.'.js';
506+
$vendorPath = $this->vendorDir.'/'.$packageName;
507+
// add an extension of there is none
508+
if (!str_contains($packageName, '.')) {
509+
$vendorPath .= '.'.$importMapType;
510+
}
450511

451512
@mkdir(\dirname($vendorPath), 0777, true);
452513
file_put_contents($vendorPath, $packageContents);

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function render(string $entryPoint = null, array $attributes = []): strin
6565
foreach ($this->importMapManager->getModulesToPreload() as $url) {
6666
$url = $this->escapeAttributeValue($url);
6767

68-
$output .= "\n<link rel=\"modulepreload\" href=\"{$url}\">";
68+
$output .= "\n<link rel=\"modulepreload\" href=\"$url\">";
6969
}
7070

7171
if (null !== $entryPoint) {
@@ -75,6 +75,18 @@ public function render(string $entryPoint = null, array $attributes = []): strin
7575
return $output;
7676
}
7777

78+
public function renderLinkTags(): string
79+
{
80+
$output = '';
81+
foreach ($this->importMapManager->getLinkTags() as $url) {
82+
$url = $this->escapeAttributeValue($url);
83+
84+
$output .= "\n<link rel=\"stylesheet\" href=\"$url\">";
85+
}
86+
87+
return $output;
88+
}
89+
7890
private function escapeAttributeValue(string $value): string
7991
{
8092
return htmlspecialchars($value, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset);

0 commit comments

Comments
 (0)
0