diff --git a/src/Symfony/Component/AssetMapper/AssetMapper.php b/src/Symfony/Component/AssetMapper/AssetMapper.php
index 85ebea3a08225..a500f008a5c26 100644
--- a/src/Symfony/Component/AssetMapper/AssetMapper.php
+++ b/src/Symfony/Component/AssetMapper/AssetMapper.php
@@ -267,7 +267,7 @@ private function loadManifest(): array
if (null === $this->manifestData) {
$path = $this->getPublicAssetsFilesystemPath().'/'.self::MANIFEST_FILE_NAME;
- if (!file_exists($path)) {
+ if (!is_file($path)) {
$this->manifestData = [];
} else {
$this->manifestData = json_decode(file_get_contents($path), true);
diff --git a/src/Symfony/Component/AssetMapper/AssetMapperRepository.php b/src/Symfony/Component/AssetMapper/AssetMapperRepository.php
index b7440a9263844..70ef44b000060 100644
--- a/src/Symfony/Component/AssetMapper/AssetMapperRepository.php
+++ b/src/Symfony/Component/AssetMapper/AssetMapperRepository.php
@@ -54,7 +54,7 @@ public function find(string $logicalPath): ?string
}
$file = rtrim($path, '/').'/'.$localLogicalPath;
- if (file_exists($file)) {
+ if (is_file($file)) {
return realpath($file);
}
}
diff --git a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php
index b6ed6c8dc9c65..d0cb9f64631ad 100644
--- a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php
+++ b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php
@@ -66,13 +66,54 @@ protected function execute(InputInterface $input, OutputInterface $output): int
throw new InvalidArgumentException(sprintf('The public directory "%s" does not exist.', $publicDir));
}
+ $outputDir = $publicDir.$this->assetMapper->getPublicPrefix();
if ($input->getOption('clean')) {
- $outputDir = $publicDir.$this->assetMapper->getPublicPrefix();
$io->comment(sprintf('Cleaning %s', $outputDir));
$this->filesystem->remove($outputDir);
$this->filesystem->mkdir($outputDir);
}
+ $manifestPath = $publicDir.$this->assetMapper->getPublicPrefix().AssetMapper::MANIFEST_FILE_NAME;
+ if (is_file($manifestPath)) {
+ $this->filesystem->remove($manifestPath);
+ }
+ $manifest = $this->createManifestAndWriteFiles($io, $publicDir);
+ $this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT));
+ $io->comment(sprintf('Manifest written to %s', $manifestPath));
+
+ $importMapPath = $outputDir.ImportMapManager::IMPORT_MAP_FILE_NAME;
+ if (is_file($importMapPath)) {
+ $this->filesystem->remove($importMapPath);
+ }
+ $this->filesystem->dumpFile($importMapPath, $this->importMapManager->getImportMapJson());
+
+ $importMapPreloadPath = $outputDir.ImportMapManager::IMPORT_MAP_PRELOAD_FILE_NAME;
+ if (is_file($importMapPreloadPath)) {
+ $this->filesystem->remove($importMapPreloadPath);
+ }
+ $this->filesystem->dumpFile(
+ $importMapPreloadPath,
+ json_encode($this->importMapManager->getModulesToPreload(), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)
+ );
+ $io->comment(sprintf('Import map written to %s and %s for quick importmap dumping onto the page.', $this->shortenPath($importMapPath), $this->shortenPath($importMapPreloadPath)));
+
+ if ($this->isDebug) {
+ $io->warning(sprintf(
+ 'You are compiling assets in development. Symfony will not serve any changed assets until you delete the "%s" directory.',
+ $this->shortenPath($outputDir)
+ ));
+ }
+
+ return 0;
+ }
+
+ private function shortenPath(string $path): string
+ {
+ return str_replace($this->projectDir.'/', '', $path);
+ }
+
+ private function createManifestAndWriteFiles(SymfonyStyle $io, string $publicDir): array
+ {
$allAssets = $this->assetMapper->allAssets();
$io->comment(sprintf('Compiling %d assets to %s%s', \count($allAssets), $publicDir, $this->assetMapper->getPublicPrefix()));
@@ -88,23 +129,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->filesystem->dumpFile($targetPath, $asset->getContent());
$manifest[$asset->logicalPath] = $asset->getPublicPath();
}
+ ksort($manifest);
- $manifestPath = $publicDir.$this->assetMapper->getPublicPrefix().AssetMapper::MANIFEST_FILE_NAME;
- $this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT));
- $io->comment(sprintf('Manifest written to %s', $manifestPath));
-
- $importMapPath = $publicDir.$this->assetMapper->getPublicPrefix().ImportMapManager::IMPORT_MAP_FILE_NAME;
- $this->filesystem->dumpFile($importMapPath, $this->importMapManager->getImportMapJson());
- $io->comment(sprintf('Import map written to %s', $importMapPath));
-
- if ($this->isDebug) {
- $io->warning(sprintf(
- 'You are compiling assets in development. Symfony will not serve any changed assets until you delete %s and %s.',
- $manifestPath,
- $importMapPath
- ));
- }
-
- return 0;
+ return $manifest;
}
}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
index 27b5dab82b103..d71496f4a2281 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
@@ -49,6 +49,7 @@ class ImportMapManager
*/
private const PACKAGE_PATTERN = '/^(?:https?:\/\/[\w\.-]+\/)?(?:(?\w+):)?(?(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*)(?:@(?[\w\._-]+))?(?:(?\/.*))?$/';
public const IMPORT_MAP_FILE_NAME = 'importmap.json';
+ public const IMPORT_MAP_PRELOAD_FILE_NAME = 'importmap.preload.json';
private array $importMapEntries;
private array $modulesToPreload;
@@ -125,9 +126,11 @@ private function buildImportMapJson(): void
return;
}
- $dumpedPath = $this->assetMapper->getPublicAssetsFilesystemPath().'/'.self::IMPORT_MAP_FILE_NAME;
- if (file_exists($dumpedPath)) {
- $this->json = file_get_contents($dumpedPath);
+ $dumpedImportMapPath = $this->assetMapper->getPublicAssetsFilesystemPath().'/'.self::IMPORT_MAP_FILE_NAME;
+ $dumpedModulePreloadPath = $this->assetMapper->getPublicAssetsFilesystemPath().'/'.self::IMPORT_MAP_PRELOAD_FILE_NAME;
+ if (is_file($dumpedImportMapPath) && is_file($dumpedModulePreloadPath)) {
+ $this->json = file_get_contents($dumpedImportMapPath);
+ $this->modulesToPreload = json_decode(file_get_contents($dumpedModulePreloadPath), true, 512, \JSON_THROW_ON_ERROR);
return;
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php
index 810132c7bfe65..6475a9110276f 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Command/AssetsMapperCompileCommandTest.php
@@ -40,6 +40,13 @@ public function testAssetsAreCompiled()
{
$application = new Application($this->kernel);
+ $targetBuildDir = $this->kernel->getProjectDir().'/public/assets';
+ // put old "built" versions to make sure the system skips using these
+ $this->filesystem->mkdir($targetBuildDir);
+ file_put_contents($targetBuildDir.'/manifest.json', '{}');
+ file_put_contents($targetBuildDir.'/importmap.json', '{"imports": {}}');
+ file_put_contents($targetBuildDir.'/importmap.preload.json', '{}');
+
$command = $application->find('asset-map:compile');
$tester = new CommandTester($command);
$res = $tester->execute([]);
@@ -47,7 +54,6 @@ public function testAssetsAreCompiled()
// match Compiling \d+ assets
$this->assertMatchesRegularExpression('/Compiling \d+ assets/', $tester->getDisplay());
- $targetBuildDir = $this->kernel->getProjectDir().'/public/assets';
$this->assertFileExists($targetBuildDir.'/subdir/file5-f4fdc37375c7f5f2629c5659a0579967.js');
$this->assertSame(<<in($targetBuildDir)->files();
- $this->assertCount(9, $finder);
+ $this->assertCount(10, $finder);
$this->assertFileExists($targetBuildDir.'/manifest.json');
- $expected = [
+ $this->assertSame([
+ 'already-abcdefVWXYZ0123456789.digested.css',
'file1.css',
'file2.js',
'file3.css',
- 'subdir/file6.js',
- 'subdir/file5.js',
'file4.js',
- 'already-abcdefVWXYZ0123456789.digested.css',
- ];
- $actual = array_keys(json_decode(file_get_contents($targetBuildDir.'/manifest.json'), true));
- sort($expected);
- sort($actual);
+ 'subdir/file5.js',
+ 'subdir/file6.js',
+ ], array_keys(json_decode(file_get_contents($targetBuildDir.'/manifest.json'), true)));
- $this->assertSame($expected, $actual);
$this->assertFileExists($targetBuildDir.'/importmap.json');
+ $actualImportMap = json_decode(file_get_contents($targetBuildDir.'/importmap.json'), true);
+ $this->assertSame([
+ '@hotwired/stimulus',
+ 'lodash',
+ 'file6',
+ '/assets/subdir/file5.js', // imported by file6
+ '/assets/file4.js', // imported by file5
+ ], array_keys($actualImportMap['imports']));
+
+ $this->assertFileExists($targetBuildDir.'/importmap.preload.json');
+ $actualPreload = json_decode(file_get_contents($targetBuildDir.'/importmap.preload.json'), true);
+ $this->assertCount(4, $actualPreload);
+ $this->assertStringStartsWith('https://unpkg.com/@hotwired/stimulus', $actualPreload[0]);
+ $this->assertStringStartsWith('/assets/subdir/file6-', $actualPreload[1]);
+ $this->assertStringStartsWith('/assets/subdir/file5-', $actualPreload[2]);
+ $this->assertStringStartsWith('/assets/file4-', $actualPreload[3]);
}
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
index 715c86edd0692..e47a5f233123b 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
@@ -84,6 +84,9 @@ public function testGetImportMapJsonUsesDumpedFile()
'@hotwired/stimulus' => 'https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js',
'app' => '/assets/app-ea9ebe6156adc038aba53164e2be0867.js',
]], json_decode($manager->getImportMapJson(), true));
+ $this->assertEquals([
+ '/assets/app-ea9ebe6156adc038aba53164e2be0867.js',
+ ], $manager->getModulesToPreload());
}
/**
diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php
new file mode 100644
index 0000000000000..9806750ba2413
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+return [
+ '@hotwired/stimulus' => [
+ 'url' => 'https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js',
+ 'preload' => true,
+ ],
+ 'lodash' => [
+ 'url' => 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js',
+ 'preload' => false,
+ ],
+ 'file6' => [
+ 'path' => 'subdir/file6.js',
+ 'preload' => true,
+ ],
+];
diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/test_public/final-assets/importmap.preload.json b/src/Symfony/Component/AssetMapper/Tests/fixtures/test_public/final-assets/importmap.preload.json
new file mode 100644
index 0000000000000..ae6114c616115
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/test_public/final-assets/importmap.preload.json
@@ -0,0 +1,3 @@
+[
+ "/assets/app-ea9ebe6156adc038aba53164e2be0867.js"
+]