From f9f7274da33669613f89ce333c830c2409a6c79b Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 3 May 2023 13:37:09 -0400 Subject: [PATCH] [AssetMapper] Fixing 2 bugs related to the compile command and importmaps --- .../Component/AssetMapper/AssetMapper.php | 2 +- .../AssetMapper/AssetMapperRepository.php | 2 +- .../Command/AssetMapperCompileCommand.php | 62 +++++++++++++------ .../ImportMap/ImportMapManager.php | 9 ++- .../AssetsMapperCompileCommandTest.php | 40 ++++++++---- .../Tests/ImportMap/ImportMapManagerTest.php | 3 + .../AssetMapper/Tests/fixtures/importmap.php | 25 ++++++++ .../final-assets/importmap.preload.json | 3 + 8 files changed, 112 insertions(+), 34 deletions(-) create mode 100644 src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php create mode 100644 src/Symfony/Component/AssetMapper/Tests/fixtures/test_public/final-assets/importmap.preload.json 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" +]