8000 feature #51543 [AssetMapper] Add support for CSS files in the importm… · symfony/symfony@4f6f404 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4f6f404

Browse files
feature #51543 [AssetMapper] Add support for CSS files in the importmap (weaverryan)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [AssetMapper] Add support for CSS files in the importmap | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | fixes #51417 (fixed this along the way) | New feature? | yes | Deprecations? | no | Tickets | None | License | MIT | Doc PR | TODO - but important! Hi! This is the brainchild of `@nicolas`-grekas & I (mostly him). This PR adds 2 big user-facing features: ## A) CSS Handling including ability to import CSS The `importmap.php` now supports a `'type' => 'css'`: ```php return [ 'app.css' => [ 'path' => 'styles/app.css', 'type' => 'css', ], ``` This, by itself, won't cause the CSS file to be loaded. But it WILL be added to the importmap, though the exact behavior will depend on the entrypoint (see next section). But, in the simplest case, it will output something like this, which adds the `<link rel="stylesheet">` tag to the page if this file is every imported in JS `import 'app.css'`. ```html <script type="importmap">{"imports": { "app.css": "data:application/javascript,const%20d%3Ddocument%2Cl%3Dd.createElement%28%22link%22%29%3Bl.href%3D%22%2Fassets%2Fstyles%2Fapp-f86cc618674db9b39b179e440063aab4.css%22%2Cl.rel%3D%22stylesheet%22%2C%28d.head%7C%7Cd.getElementsByTagName%28%22head%22%29%5B0%5D%29.appendChild%28l%29" }}</script> ``` More commonly, in the same way that AssetMapper finds relative JavaScript imports and adds them to the importmap, it also finds relative CSS imports and adds *those* to the importmap. This allows you to: `import './styles/foo.css'` without needing to add `foo.css` to the importmap. It "just works". This would result in `foo.css` being added to the page via JavaScript unless it is in the import chain of an entrypoint (see next section), in which case it would be a true `<link>` tag. Also, you can now download CSS files via `importmap:require` (or, if a package comes with a CSS file, it will be downloaded and added automatically - i.e. if the package has a `style` key): ```bash # will download `bootstrap` AND `bootstrap/dist/css/bootstrap.min.css` php bin/console importmap:require bootstrap ``` ## B) Auto-preload based on entrypoints Like with Webpack, there is now a concept of "entrypoints". The ONE time you call `{{ importmap() }}` on the page, you will pass the 1 or many "entrypoint" names from `importmap.php` that should be loaded - e.g. `importmap('app')` (the most common) or `importmap(['app', 'checkout'])`. Most simply (and this is true already in AssetMapper), this causes a `<script type="module">import 'app';</script>` to be rendered into the page. But in this PR, it also has another critical role: * For each entrypoint, all of the non-lazy JavaScript dependencies are found. So if `app.js` imports `other.js` imports `yet-another.js` imports `some_styles.css`, using non-lazy imports (i.e. `import './other.js` vs the lazy `import('./other.js')`), then `other.js`, `yet-another.js` and `some_styles.css` are all returned. * For all of these dependencies, they are "preloaded" * `other.js` -> preloaded (i.e. `modulepreload` tag rendered) * `yet-another.js` -> preloaded (i.e. `modulepreload` tag rendered) * `some_styles.css` "preloaded" - i.e. a `<link rel="stylesheet"> is added to the page. The idea is that, if `app.js` is your entrypoint, then *every* non-lazy import in its import chain will need to be downloaded before the JavaScript starts executing. So all files should be preloaded. Additionally, if we find any CSS that is imported in a non-lazy way, we render those as `link` tags. The `preload` option in `importmap.php` is GONE. Preloading is controlled entirely through the entrypoint. This entrypoint logic also affects the ordering of the non-lazy CSS files (i.e. the CSS files that will be rendered as `link` tags). It finds all (in order) non-lazy imported CSS files from the entrypoint and render them as `link` tags in order (like Webpack). I propose the recipe starting `importmap.php` is updated to be: ```php return [ 'app' => [ 'path' => 'app.js', 'entrypoint' => true, ], ]; ``` And then in `assets/app.js`: ```js import './styles/app.css'; ``` ## C) Other Improvements * Fixed #51417 where deploying to a subdirectory didn't work * Added a new event in `asset-map:compile` so other processes can hook into this (will make bundles like TailwindBundle) nicer. * Removed `importmap:export` command: I don't see a need for this * Added basic, conservative checking for commented-out imports - if lines START with `//` or `/*` ``` // both will correctly be ignored // import 'foo'; /* import 'foo'; */ ``` ### TODOs * [ ] Update the 6.4 docs * [ ] update the 6.4 recipe Commits ------- 5060fa1 [AssetMapper] Add support for CSS files in the importmap
2 parents 4b21e21 + 5060fa1 commit 4f6f404

File tree

55 files changed

+2306
-788
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2306
-788
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
6.4
5+
---
6+
7+
* Allow an array to be passed as the first argument to the `importmap()` Twig function
8+
49
6.3
510
---
611

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ public function __construct(private readonly ImportMapRenderer $importMapRendere
2222
{
2323
}
2424

25-
public function importmap(?string $entryPoint = 'app', array $attributes = []): string
25+
public function importmap(string|array|null $entryPoint = 'app', array $attributes = []): string
2626
{
27+
if (null === $entryPoint) {
28+
trigger_deprecation('symfony/twig-bridge', '6.4', 'Passing null as the first argument of the "importmap" Twig function is deprecated, pass an empty array if no entrypoints are desired.');
29+
}
30+
2731
return $this->importMapRenderer->render($entryPoint, $attributes);
2832
}
2933
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 8 additions & 4 deletions
F438
Original file line numberDiff line numberDiff line change
@@ -1339,23 +1339,27 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
13391339
->setArgument(0, $config['missing_import_mode']);
13401340

13411341
$container->getDefinition('asset_mapper.compiler.javascript_import_path_compiler')
1342-
->setArgument(0, $config['missing_import_mode']);
1342+
->setArgument(1, $config['missing_import_mode']);
13431343

13441344
$container
13451345
->getDefinition('asset_mapper.importmap.manager')
1346-
->replaceArgument(2, $config['importmap_path'])
13471346
->replaceArgument(3, $config['vendor_dir'])
13481347
;
13491348

1349+
$container
1350+
->getDefinition('asset_mapper.importmap.config_reader')
1351+
->replaceArgument(0, $config['importmap_path'])
1352+
;
1353+
13501354
$container
13511355
->getDefinition('asset_mapper.importmap.resolver')
13521356
->replaceArgument(0, $config['provider'])
13531357
;
13541358

13551359
$container
13561360
->getDefinition('asset_mapper.importmap.renderer')
1357-
->replaceArgument(2, $config['importmap_polyfill'] ?? ImportMapManager::POLYFILL_URL)
1358-
->replaceArgument(3, $config['importmap_script_attributes'])
1361+
->replaceArgument(3, $config['importmap_polyfill'] ?? ImportMapManager::POLYFILL_URL)
1362+
->replaceArgument(4, $config['importmap_script_attributes'])
13591363
;
13601364

13611365
$container->registerForAutoconfiguration(PackageResolverInterface::class)

src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use Symfony\Component\AssetMapper\AssetMapperRepository;
1919
use Symfony\Component\AssetMapper\Command\AssetMapperCompileCommand;
2020
use Symfony\Component\AssetMapper\Command\DebugAssetMapperCommand;
21-
use Symfony\Component\AssetMapper\Command\ImportMapExportCommand;
2221
use Symfony\Component\AssetMapper\Command\ImportMapInstallCommand;
2322
use Symfony\Component\AssetMapper\Command\ImportMapRemoveCommand;
2423
use Symfony\Component\AssetMapper\Command\ImportMapRequireCommand;
@@ -28,6 +27,7 @@
2827
use Symfony\Component\AssetMapper\Compiler\SourceMappingUrlsCompiler;
2928
use Symfony\Component\AssetMapper\Factory\CachedMappedAssetFactory;
3029
use Symfony\Component\AssetMapper\Factory\MappedAssetFactory;
30+
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
3131
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
3232
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
3333
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
@@ -100,6 +100,7 @@
100100
param('kernel.project_dir'),
101101
abstract_arg('public directory name'),
102102
param('kernel.debug'),
103+
service('event_dispatcher')->nullOnInvalid(),
103104
])
104105
->tag('console.command')
105106

@@ -130,17 +131,23 @@
130131

131132
->set('asset_mapper.compiler.javascript_import_path_compiler', JavaScriptImportPathCompiler::class)
132133
->args([
134+
service('asset_mapper.importmap.manager'),
133135
abstract_arg('missing import mode'),
134136
service('logger'),
135137
])
136138
->tag('asset_mapper.compiler')
137139
->tag('monolog.logger', ['channel' => 'asset_mapper'])
138140

141+
->set('asset_mapper.importmap.config_reader', ImportMapConfigReader::class)
142+
->args([
143+
abstract_arg('importmap.php path'),
144+
])
145+
139146
->set('asset_mapper.importmap.manager', ImportMapManager::class)
140147
->args([
141148
service('asset_mapper'),
142149
service('asset_mapper.public_assets_path_resolver'),
143-
abstract_arg('importmap.php path'),
150+
service('asset_mapper.importmap.config_reader'),
144151
abstract_arg('vendor directory'),
145152
service('asset_mapper.importmap.resolver'),
146153
service('http_client'),
@@ -180,6 +187,7 @@
180187
->set('asset_mapper.importmap.renderer', ImportMapRenderer::class)
181188
->args([
182189
service('asset_mapper.importmap.manager'),
190+
service('assets.packages')->nullOnInvalid(),
183191
param('kernel.charset'),
184192
abstract_arg('polyfill URL'),
185193
abstract_arg('script HTML attributes'),
@@ -201,10 +209,6 @@
201209
->args([service('asset_mapper.importmap.manager')])
202210
->tag('console.command')
203211

204-
->set('asset_mapper.importmap.command.export', ImportMapExportCommand::class)
205-
->args([service('asset_mapper.importmap.manager')])
206-
->tag('console.command')
207-
208212
->set('asset_mapper.importmap.command.install', ImportMapInstallCommand::class)
209213
->args([service('asset_mapper.importmap.manager')])
210214
->tag('console.command')

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/XmlFrameworkExtensionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function testAssetMapper()
8585
$this->assertSame(['zip' => 'application/zip'], $definition->getArgument(2));
8686

8787
$definition = $container->getDefinition('asset_mapper.importmap.renderer');
88-
$this->assertSame(['data-turbo-track' => 'reload'], $definition->getArgument(3));
88+
$this->assertSame(['data-turbo-track' => 'reload'], $definition->getArgument(4));
8989

9090
$definition = $container->getDefinition('asset_mapper.repository');
9191
$this->assertSame(['assets/' => '', 'assets2/' => 'my_namespace'], $definition->getArgument(0));

src/Symfony/Component/AssetMapper/AssetDependency.php

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/Symfony/Component/AssetMapper/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ CHANGELOG
55
---
66

77
* Mark the component as non experimental
8+
* Add CSS support to the importmap
9+
* Add "entrypoints" concept to the importmap
10+
* Add `PreAssetsCompileEvent` event when running `asset-map:compile`
11+
* Add support for importmap paths to use the Asset component (for subdirectories)
12+
* Removed the `importmap:export` command
813
* Add a `importmap:install` command to download all missing downloaded packages
914
* Allow specifying packages to update for the `importmap:update` command
1015

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

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\AssetMapper\AssetMapper;
1515
use Symfony\Component\AssetMapper\AssetMapperInterface;
16+
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
1617
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
1718
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
1819
use Symfony\Component\Console\Attribute\AsCommand;
@@ -22,6 +23,7 @@
2223
use Symfony\Component\Console\Output\OutputInterface;
2324
use Symfony\Component\Console\Style\SymfonyStyle;
2425
use Symfony\Component\Filesystem\Filesystem;
26+
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
2527

2628
/**
2729
* Compiles the assets in the asset mapper to the final output directory.
@@ -41,6 +43,7 @@ public function __construct(
4143
private readonly string $projectDir,
4244
private readonly string $publicDirName,
4345
private readonly bool $isDebug,
46+
private readonly ?EventDispatcherInterface $eventDispatcher = null,
4447
) {
4548
parent::__construct();
4649
}
@@ -73,29 +76,44 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7376
$this->filesystem->mkdir($outputDir);
7477
}
7578

79+
// set up the file paths
80+
$files = [];
7681
$manifestPath = $outputDir.'/'.AssetMapper::MANIFEST_FILE_NAME;
77-
if (is_file($manifestPath)) {
78-
$this->filesystem->remove($manifestPath);
82+
$files[] = $manifestPath;
83+
84+
$importMapPath = $outputDir.'/'.ImportMapManager::IMPORT_MAP_CACHE_FILENAME;
85+
$files[] = $importMapPath;
86+
87+
$entrypointFilePaths = [];
88+
foreach ($this->importMapManager->getEntrypointNames() as $entrypointName) {
89+
$dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapManager::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
90+
$files[] = $dumpedEntrypointPath;
91+
$entrypointFilePaths[$entrypointName] = $dumpedEntrypointPath;
92+
}
93+
94+
// remove existing files
95+
foreach ($files as $file) {
96+
if (is_file($file)) {
97+
$this->filesystem->remove($file);
98+
}
7999
}
100+
101+
$this->eventDispatcher?->dispatch(new PreAssetsCompileEvent($outputDir, $output));
102+
103+
// dump new files
80104
$manifest = $this->createManifestAndWriteFiles($io, $publicDir);
81105
$this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT));
82106
$io->comment(sprintf('Manifest written to <info>%s</info>', $this->shortenPath($manifestPath)));
83107

84-
$importMapPath = $outputDir.'/'.ImportMapManager::IMPORT_MAP_FILE_NAME;
85-
if (is_file($importMapPath)) {
86-
$this->filesystem->remove($importMapPath);
87-
}
88-
$this->filesystem->dumpFile($importMapPath, $this->importMapManager->getImportMapJson());
108+
$this->filesystem->dumpFile($importMapPath, json_encode($this->importMapManager->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
109+
$io->comment(sprintf('Import map data written to <info>%s</info>.', $this->shortenPath($importMapPath)));
89110

90-
$importMapPreloadPath = $outputDir.'/'.ImportMapManager::IMPORT_MAP_PRELOAD_FILE_NAME;
91-
if (is_file($importMapPreloadPath)) {
92-
$this->filesystem->remove($importMapPreloadPath);
111+
$entrypointNames = $this->importMapManager->getEntrypointNames();
112+
foreach ($entrypointFilePaths as $entrypointName => $path) {
113+
$this->filesystem->dumpFile($path, json_encode($this->importMapManager->getEntrypointMetadata($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
93114
}
94-
$this->filesystem->dumpFile(
95-
$importMapPreloadPath,
96-
json_encode($this->importMapManager->getModulesToPreload(), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)
97-
);
98-
$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)));
115+
$styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('<info>%s</>', $entrypointName), $entrypointNames);
116+
$io->comment(sprintf('Entrypoint metadata written for <comment>%d</> entrypoints (%s).', \count($entrypointNames), implode(', ', $styledEntrypointNames)));
99117

100118
if ($this->isDebug) {
101119
$io->warning(sprintf(

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

Lines changed: 0 additions & 38 deletions
This file was deleted.

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,14 @@ protected function configure(): void
4343
$this
4444
->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to add')
4545
->addOption('download', 'd', InputOption::VALUE_NONE, 'Download packages locally')
46-
->addOption('preload', 'p', InputOption::VALUE_NONE, 'Preload packages')
4746
->addOption('path', null, InputOption::VALUE_REQUIRED, 'The local path where the package lives relative to the project root')
4847
->setHelp(<<<'EOT'
4948
The <info>%command.name%</info> command adds packages to <comment>importmap.php</comment> usually
5049
by finding a CDN URL for the given package and version.
5150
5251
For example:
5352
54-
<info>php %command.full_name% lodash --preload</info>
53+
<info>php %command.full_name% lodash</info>
5554
<info>php %command.full_name% "lodash@^4.15"</info>
5655
5756
You can also require specific paths of a package:
@@ -62,10 +61,6 @@ protected function configure(): void
6261
6362
<info>php %command.full_name% "vue/dist/vue.esm-bundler.js=vue"</info>
6463
65-
The <info>preload</info> option will set the <info>preload</info> option in the importmap,
66-
which will tell the browser to preload the package. This should be used for all
67-
critical packages that are needed on page load.
68-
6964
The <info>download</info> option will download the package locally and point the
7065
importmap to it. Use this if you want to avoid using a CDN or if you want to
7166
ensure that the package is available even if the CDN is down.
@@ -119,17 +114,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
119114
$parts['package'],
120115
$parts['version'] ?? null,
121116
$input->getOption('download'),
122-
$input->getOption('preload'),
123117
$parts['alias'] ?? $parts['package'],
124118
isset($parts['registry']) && $parts['registry'] ? $parts['registry'] : null,
125119
$path,
126120
);
127121
}
128122

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-
133123
$newPackages = $this->importMapManager->require($packages);
134124
if (1 === \count($newPackages)) {
135125
$newPackage = $newPackages[0];
@@ -151,7 +141,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
151141
$message .= '.';
152142
} else {
153143
$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));
144+
$message = sprintf('%d new items (%s) added to the importmap.php!', \count($newPackages), implode(', ', $names));
155145
}
156146

157147
$messages = [$message];

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
namespace Symfony\Component\AssetMapper\Compiler;
1313

1414
use Psr\Log\LoggerInterface;
15-
use Symfony\Component\AssetMapper\AssetDependency;
1615
use Symfony\Component\AssetMapper\AssetMapperInterface;
1716
use Symfony\Component\AssetMapper\Exception\RuntimeException;
1817
use Symfony\Component\AssetMapper\MappedAsset;
@@ -54,7 +53,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
5453
return $matches[0];
5554
}
5655

57-
$asset->addDependency(new AssetDependency($dependentAsset));
56+
$asset->addDependency($dependentAsset);
5857
$relativePath = $this->createRelativePath($asset->publicPathWithoutDigest, $dependentAsset->publicPath);
5958

6059
return 'url("'.$relativePath.'")';

0 commit comments

Comments
 (0)
0