diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
index d31573031c656..046bd8c6c5fde 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
@@ -31,6 +31,7 @@
use Symfony\Component\AssetMapper\Factory\MappedAssetFactory;
use Symfony\Component\AssetMapper\ImportMap\ImportMapAuditor;
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator;
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
@@ -101,7 +102,7 @@
->args([
service('asset_mapper.public_assets_path_resolver'),
service('asset_mapper'),
- service('asset_mapper.importmap.manager'),
+ service('asset_mapper.importmap.generator'),
service('filesystem'),
param('kernel.project_dir'),
abstract_arg('public directory name'),
@@ -137,7 +138,7 @@
->set('asset_mapper.compiler.javascript_import_path_compiler', JavaScriptImportPathCompiler::class)
->args([
- service('asset_mapper.importmap.manager'),
+ service('asset_mapper.importmap.config_reader'),
abstract_arg('missing import mode'),
service('logger'),
])
@@ -153,13 +154,19 @@
->set('asset_mapper.importmap.manager', ImportMapManager::class)
->args([
service('asset_mapper'),
- service('asset_mapper.public_assets_path_resolver'),
service('asset_mapper.importmap.config_reader'),
service('asset_mapper.importmap.remote_package_downloader'),
service('asset_mapper.importmap.resolver'),
])
->alias(ImportMapManager::class, 'asset_mapper.importmap.manager')
+ ->set('asset_mapper.importmap.generator', ImportMapGenerator::class)
+ ->args([
+ service('asset_mapper'),
+ service('asset_mapper.public_assets_path_resolver'),
+ service('asset_mapper.importmap.config_reader'),
+ ])
+
->set('asset_mapper.importmap.remote_package_storage', RemotePackageStorage::class)
->args([
abstract_arg('vendor directory'),
@@ -183,7 +190,7 @@
->set('asset_mapper.importmap.renderer', ImportMapRenderer::class)
->args([
- service('asset_mapper.importmap.manager'),
+ service('asset_mapper.importmap.generator'),
service('assets.packages')->nullOnInvalid(),
param('kernel.charset'),
abstract_arg('polyfill URL'),
@@ -205,7 +212,6 @@
->set('asset_mapper.importmap.command.require', ImportMapRequireCommand::class)
->args([
service('asset_mapper.importmap.manager'),
- param('kernel.project_dir'),
service('asset_mapper.importmap.version_checker'),
])
->tag('console.command')
diff --git a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php
index a2fbc2d3e4709..ab5bf73916cfe 100644
--- a/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php
+++ b/src/Symfony/Component/AssetMapper/Command/AssetMapperCompileCommand.php
@@ -14,7 +14,7 @@
use Symfony\Component\AssetMapper\AssetMapper;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Event\PreAssetsCompileEvent;
-use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -38,7 +38,7 @@ final class AssetMapperCompileCommand extends Command
public function __construct(
private readonly PublicAssetsPathResolverInterface $publicAssetsPathResolver,
private readonly AssetMapperInterface $assetMapper,
- private readonly ImportMapManager $importMapManager,
+ private readonly ImportMapGenerator $importMapGenerator,
private readonly Filesystem $filesystem,
private readonly string $projectDir,
private readonly string $publicDirName,
@@ -81,12 +81,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$manifestPath = $outputDir.'/'.AssetMapper::MANIFEST_FILE_NAME;
$files[] = $manifestPath;
- $importMapPath = $outputDir.'/'.ImportMapManager::IMPORT_MAP_CACHE_FILENAME;
+ $importMapPath = $outputDir.'/'.ImportMapGenerator::IMPORT_MAP_CACHE_FILENAME;
$files[] = $importMapPath;
$entrypointFilePaths = [];
- foreach ($this->importMapManager->getEntrypointNames() as $entrypointName) {
- $dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapManager::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
+ foreach ($this->importMapGenerator->getEntrypointNames() as $entrypointName) {
+ $dumpedEntrypointPath = $outputDir.'/'.sprintf(ImportMapGenerator::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entrypointName);
$files[] = $dumpedEntrypointPath;
$entrypointFilePaths[$entrypointName] = $dumpedEntrypointPath;
}
@@ -105,12 +105,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->filesystem->dumpFile($manifestPath, json_encode($manifest, \JSON_PRETTY_PRINT));
$io->comment(sprintf('Manifest written to %s', $this->shortenPath($manifestPath)));
- $this->filesystem->dumpFile($importMapPath, json_encode($this->importMapManager->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
+ $this->filesystem->dumpFile($importMapPath, json_encode($this->importMapGenerator->getRawImportMapData(), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
$io->comment(sprintf('Import map data written to %s.', $this->shortenPath($importMapPath)));
- $entrypointNames = $this->importMapManager->getEntrypointNames();
+ $entrypointNames = $this->importMapGenerator->getEntrypointNames();
foreach ($entrypointFilePaths as $entrypointName => $path) {
- $this->filesystem->dumpFile($path, json_encode($this->importMapManager->getEntrypointMetadata($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
+ $this->filesystem->dumpFile($path, json_encode($this->importMapGenerator->findEagerEntrypointImports($entrypointName), \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG));
}
$styledEntrypointNames = array_map(fn (string $entrypointName) => sprintf('%s>', $entrypointName), $entrypointNames);
$io->comment(sprintf('Entrypoint metadata written for %d> entrypoints (%s).', \count($entrypointNames), implode(', ', $styledEntrypointNames)));
diff --git a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php
index a7402ee92020a..19b5dfbbe4ba6 100644
--- a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php
+++ b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php
@@ -33,7 +33,6 @@ final class ImportMapRequireCommand extends Command
public function __construct(
private readonly ImportMapManager $importMapManager,
- private readonly string $projectDir,
private readonly ImportMapVersionChecker $importMapVersionChecker,
) {
parent::__construct();
diff --git a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
index f1f33d71eedc0..481cbb9b90a69 100644
--- a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
+++ b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
@@ -15,7 +15,7 @@
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\Exception\CircularAssetsException;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
-use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport;
use Symfony\Component\AssetMapper\MappedAsset;
@@ -32,7 +32,7 @@ final class JavaScriptImportPathCompiler implements AssetCompilerInterface
private const IMPORT_PATTERN = '/(?:import\s*(?:(?:\*\s*as\s+\w+|[\w\s{},*]+)\s*from\s*)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]?/m';
public function __construct(
- private readonly ImportMapManager $importMapManager,
+ private readonly ImportMapConfigReader $importMapConfigReader,
private readonly string $missingImportMode = self::MISSING_IMPORT_WARN,
private readonly ?LoggerInterface $logger = null,
) {
@@ -139,7 +139,7 @@ private function isCommentedOut(mixed $offsetStart, string $fullContent): bool
private function findAssetForBareImport(string $importedModule, AssetMapperInterface $assetMapper): ?MappedAsset
{
- if (!$importMapEntry = $this->importMapManager->findRootImportMapEntry($importedModule)) {
+ if (!$importMapEntry = $this->importMapConfigReader->findRootImportMapEntry($importedModule)) {
// don't warn on missing non-relative (bare) imports: these could be valid URLs
return null;
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php
index 8aaee7a3e1646..d282fe1bac747 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php
@@ -130,6 +130,13 @@ public function writeEntries(ImportMapEntries $entries): void
EOF);
}
+ public function findRootImportMapEntry(string $moduleName): ?ImportMapEntry
+ {
+ $entries = $this->getEntries();
+
+ return $entries->has($moduleName) ? $entries->get($moduleName) : null;
+ }
+
public function createRemoteEntry(string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint): ImportMapEntry
{
$path = $this->remotePackageStorage->getDownloadPath($packageModuleSpecifier, $type);
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php
new file mode 100644
index 0000000000000..bca5897c03c53
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php
@@ -0,0 +1,255 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\AssetMapper\ImportMap;
+
+use Symfony\Component\AssetMapper\AssetMapperInterface;
+use Symfony\Component\AssetMapper\Exception\LogicException;
+use Symfony\Component\AssetMapper\MappedAsset;
+use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
+
+/**
+ * Provides data needed to write the importmap & preloads.
+ */
+class ImportMapGenerator
+{
+ public const IMPORT_MAP_CACHE_FILENAME = 'importmap.json';
+ public const ENTRYPOINT_CACHE_FILENAME_PATTERN = 'entrypoint.%s.json';
+
+ public function __construct(
+ private readonly AssetMapperInterface $assetMapper,
+ private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
+ private readonly ImportMapConfigReader $importMapConfigReader,
+ ) {
+ }
+
+ /**
+ * @internal
+ */
+ public function getEntrypointNames(): array
+ {
+ $rootEntries = $this->importMapConfigReader->getEntries();
+ $entrypointNames = [];
+ foreach ($rootEntries as $entry) {
+ if ($entry->isEntrypoint) {
+ $entrypointNames[] = $entry->importName;
+ }
+ }
+
+ return $entrypointNames;
+ }
+
+ /**
+ * @param string[] $entrypointNames
+ *
+ * @return array
+ *
+ * @internal
+ */
+ public function getImportMapData(array $entrypointNames): array
+ {
+ $rawImportMapData = $this->getRawImportMapData();
+ $finalImportMapData = [];
+ foreach ($entrypointNames as $entrypointName) {
+ $entrypointImports = $this->findEagerEntrypointImports($entrypointName);
+ // Entrypoint modules must be preloaded before their dependencies
+ foreach ([$entrypointName, ...$entrypointImports] as $import) {
+ if (isset($finalImportMapData[$import])) {
+ continue;
+ }
+
+ // Missing dependency - rely on browser or compilers to warn
+ if (!isset($rawImportMapData[$import])) {
+ continue;
+ }
+
+ $finalImportMapData[$import] = $rawImportMapData[$import];
+ $finalImportMapData[$import]['preload'] = true;
+ unset($rawImportMapData[$import]);
+ }
+ }
+
+ return array_merge($finalImportMapData, $rawImportMapData);
+ }
+
+ /**
+ * @internal
+ *
+ * @return array
+ */
+ public function getRawImportMapData(): array
+ {
+ $dumpedImportMapPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::IMPORT_MAP_CACHE_FILENAME;
+ if (is_file($dumpedImportMapPath)) {
+ return json_decode(file_get_contents($dumpedImportMapPath), true, 512, \JSON_THROW_ON_ERROR);
+ }
+
+ $allEntries = [];
+ foreach ($this->importMapConfigReader->getEntries() as $rootEntry) {
+ $allEntries[$rootEntry->importName] = $rootEntry;
+ $allEntries = $this->addImplicitEntries($rootEntry, $allEntries);
+ }
+
+ $rawImportMapData = [];
+ foreach ($allEntries as $entry) {
+ $asset = $this->findAsset($entry->path);
+ if (!$asset) {
+ throw $this->createMissingImportMapAssetException($entry);
+ }
+
+ $path = $asset->publicPath;
+ $data = ['path' => $path, 'type' => $entry->type->value];
+ $rawImportMapData[$entry->importName] = $data;
+ }
+
+ return $rawImportMapData;
+ }
+
+ /**
+ * Given an importmap entry name, finds all the non-lazy module imports in its chain.
+ *
+ * @internal
+ *
+ * @return array The array of import names
+ */
+ public function findEagerEntrypointImports(string $entryName): array
+ {
+ $dumpedEntrypointPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName);
+ if (is_file($dumpedEntrypointPath)) {
+ return json_decode(file_get_contents($dumpedEntrypointPath), true, 512, \JSON_THROW_ON_ERROR);
+ }
+
+ $rootImportEntries = $this->importMapConfigReader->getEntries();
+ if (!$rootImportEntries->has($entryName)) {
+ throw new \InvalidArgumentException(sprintf('The entrypoint "%s" does not exist in "importmap.php".', $entryName));
+ }
+
+ if (!$rootImportEntries->get($entryName)->isEntrypoint) {
+ throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is not an entry point in "importmap.php". Set "entrypoint" => true to make it available as an entrypoint.', $entryName));
+ }
+
+ if ($rootImportEntries->get($entryName)->isRemotePackage()) {
+ throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is a remote package and cannot be used as an entrypoint.', $entryName));
+ }
+
+ $asset = $this->findAsset($rootImportEntries->get($entryName)->path);
+ if (!$asset) {
+ throw new \InvalidArgumentException(sprintf('The path "%s" of the entrypoint "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $rootImportEntries->get($entryName)->path, $entryName));
+ }
+
+ return $this->findEagerImports($asset);
+ }
+
+ /**
+ * Adds "implicit" entries to the importmap.
+ *
+ * This recursively searches the dependencies of the given entry
+ * (i.e. it looks for modules imported from other modules)
+ * and adds them to the importmap.
+ *
+ * @param array $currentImportEntries
+ *
+ * @return array
+ */
+ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportEntries): array
+ {
+ // only process import dependencies for JS files
+ if (ImportMapType::JS !== $entry->type) {
+ return $currentImportEntries;
+ }
+
+ if (!$asset = $this->findAsset($entry->path)) {
+ // should only be possible at this point for root importmap.php entries
+ throw $this->createMissingImportMapAssetException($entry);
+ }
+
+ foreach ($asset->getJavaScriptImports() as $javaScriptImport) {
+ $importName = $javaScriptImport->importName;
+
+ if (isset($currentImportEntries[$importName])) {
+ // entry already exists
+ continue;
+ }
+
+ // check if this import requires an automatic importmap entry
+ if ($javaScriptImport->addImplicitlyToImportMap && $javaScriptImport->asset) {
+ $nextEntry = ImportMapEntry::createLocal(
+ $importName,
+ ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS,
+ $javaScriptImport->asset->logicalPath,
+ false,
+ );
+
+ $currentImportEntries[$importName] = $nextEntry;
+ } else {
+ $nextEntry = $this->importMapConfigReader->findRootImportMapEntry($importName);
+ }
+
+ // unless there was some missing importmap entry, recurse
+ if ($nextEntry) {
+ $currentImportEntries = $this->addImplicitEntries($nextEntry, $currentImportEntries);
+ }
+ }
+
+ return $currentImportEntries;
+ }
+
+ private function findRootImportMapEntry(string $moduleName): ?ImportMapEntry
+ {
+ $entries = $this->importMapConfigReader->getEntries();
+
+ return $entries->has($moduleName) ? $entries->get($moduleName) : null;
+ }
+
+ /**
+ * Finds the MappedAsset allowing for a "logical path", relative or absolute filesystem path.
+ */
+ private function findAsset(string $path): ?MappedAsset
+ {
+ if ($asset = $this->assetMapper->getAsset($path)) {
+ return $asset;
+ }
+
+ if (str_starts_with($path, '.')) {
+ $path = $this->importMapConfigReader->getRootDirectory().'/'.$path;
+ }
+
+ return $this->assetMapper->getAssetFromSourcePath($path);
+ }
+
+ private function findEagerImports(MappedAsset $asset): array
+ {
+ $dependencies = [];
+ foreach ($asset->getJavaScriptImports() as $javaScriptImport) {
+ if ($javaScriptImport->isLazy) {
+ continue;
+ }
+
+ $dependencies[] = $javaScriptImport->importName;
+
+ // the import is for a MappedAsset? Follow its imports!
+ if ($javaScriptImport->asset) {
+ $dependencies = array_merge($dependencies, $this->findEagerImports($javaScriptImport->asset));
+ }
+ }
+
+ return $dependencies;
+ }
+
+ private function createMissingImportMapAssetException(ImportMapEntry $entry): \InvalidArgumentException
+ {
+ if ($entry->isRemotePackage()) {
+ throw new LogicException(sprintf('The "%s" vendor asset is missing. Try running the "importmap:install" command.', $entry->importName));
+ }
+
+ throw new LogicException(sprintf('The asset "%s" cannot be found in any asset map paths.', $entry->path));
+ }
+}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
index fc6a27bcc2dc8..4d06087a5542e 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
@@ -12,10 +12,8 @@
namespace Symfony\Component\AssetMapper\ImportMap;
use Symfony\Component\AssetMapper\AssetMapperInterface;
-use Symfony\Component\AssetMapper\Exception\LogicException;
use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface;
use Symfony\Component\AssetMapper\MappedAsset;
-use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
/**
* @author Kévin Dunglas
@@ -25,12 +23,8 @@
*/
class ImportMapManager
{
- public const IMPORT_MAP_CACHE_FILENAME = 'importmap.json';
- public const ENTRYPOINT_CACHE_FILENAME_PATTERN = 'entrypoint.%s.json';
-
public function __construct(
private readonly AssetMapperInterface $assetMapper,
- private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
private readonly ImportMapConfigReader $importMapConfigReader,
private readonly RemotePackageDownloader $packageDownloader,
private readonly PackageResolverInterface $resolver,
@@ -69,104 +63,6 @@ public function update(array $packages = []): array
return $this->updateImportMapConfig(true, [], [], $packages);
}
- public function findRootImportMapEntry(string $moduleName): ?ImportMapEntry
- {
- $entries = $this->importMapConfigReader->getEntries();
-
- return $entries->has($moduleName) ? $entries->get($moduleName) : null;
- }
-
- /**
- * @internal
- *
- * @param string[] $entrypointNames
- *
- * @return array
- */
- public function getImportMapData(array $entrypointNames): array
- {
- $rawImportMapData = $this->getRawImportMapData();
- $finalImportMapData = [];
- foreach ($entrypointNames as $entrypointName) {
- $entrypointImports = $this->findEagerEntrypointImports($entrypointName);
- // Entrypoint modules must be preloaded before their dependencies
- foreach ([$entrypointName, ...$entrypointImports] as $import) {
- if (isset($finalImportMapData[$import])) {
- continue;
- }
-
- // Missing dependency - rely on browser or compilers to warn
- if (!isset($rawImportMapData[$import])) {
- continue;
- }
-
- $finalImportMapData[$import] = $rawImportMapData[$import];
- $finalImportMapData[$import]['preload'] = true;
- unset($rawImportMapData[$import]);
- }
- }
-
- return array_merge($finalImportMapData, $rawImportMapData);
- }
-
- /**
- * @internal
- */
- public function getEntrypointMetadata(string $entrypointName): array
- {
- return $this->findEagerEntrypointImports($entrypointName);
- }
-
- /**
- * @internal
- */
- public function getEntrypointNames(): array
- {
- $rootEntries = $this->importMapConfigReader->getEntries();
- $entrypointNames = [];
- foreach ($rootEntries as $entry) {
- if ($entry->isEntrypoint) {
- $entrypointNames[] = $entry->importName;
- }
- }
-
- return $entrypointNames;
- }
-
- /**
- * @internal
- *
- * @return array
- */
- public function getRawImportMapData(): array
- {
- $dumpedImportMapPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.self::IMPORT_MAP_CACHE_FILENAME;
- if (is_file($dumpedImportMapPath)) {
- return json_decode(file_get_contents($dumpedImportMapPath), true, 512, \JSON_THROW_ON_ERROR);
- }
-
- $rootEntries = $this->importMapConfigReader->getEntries();
- $allEntries = [];
- foreach ($rootEntries as $rootEntry) {
- $allEntries[$rootEntry->importName] = $rootEntry;
- $allEntries = $this->addImplicitEntries($rootEntry, $allEntries, $rootEntries);
- }
-
- $rawImportMapData = [];
- foreach ($allEntries as $entry) {
- $asset = $this->findAsset($entry->path);
- if (!$asset) {
- throw $this->createMissingImportMapAssetException($entry);
- }
-
- $path = $asset->publicPath;
- $data = ['path' => $path, 'type' => $entry->type->value];
- $rawImportMapData[$entry->importName] = $data;
- }
-
- return $rawImportMapData;
- }
-
/**
* @internal
*/
@@ -303,112 +199,6 @@ private function cleanupPackageFiles(ImportMapEntry $entry): void
}
}
- /**
- * Adds "implicit" entries to the importmap.
- *
- * This recursively searches the dependencies of the given entry
- * (i.e. it looks for modules imported from other modules)
- * and adds them to the importmap.
- *
- * @param array $currentImportEntries
- *
- * @return array
- */
- private function addImplicitEntries(ImportMapEntry $entry, array $currentImportEntries, ImportMapEntries $rootEntries): array
- {
- // only process import dependencies for JS files
- if (ImportMapType::JS !== $entry->type) {
- return $currentImportEntries;
- }
-
- if (!$asset = $this->findAsset($entry->path)) {
- // should only be possible at this point for root importmap.php entries
- throw $this->createMissingImportMapAssetException($entry);
- }
-
- foreach ($asset->getJavaScriptImports() as $javaScriptImport) {
- $importName = $javaScriptImport->importName;
-
- if (isset($currentImportEntries[$importName])) {
- // entry already exists
- continue;
- }
-
- // check if this import requires an automatic importmap entry
- if ($javaScriptImport->addImplicitlyToImportMap && $javaScriptImport->asset) {
- $nextEntry = ImportMapEntry::createLocal(
- $importName,
- ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS,
- $javaScriptImport->asset->logicalPath,
- false,
- );
-
- $currentImportEntries[$importName] = $nextEntry;
- } else {
- $nextEntry = $this->findRootImportMapEntry($importName);
- }
-
- // unless there was some missing importmap entry, recurse
- if ($nextEntry) {
- $currentImportEntries = $this->addImplicitEntries($nextEntry, $currentImportEntries, $rootEntries);
- }
- }
-
- return $currentImportEntries;
- }
-
- /**
- * Given an importmap entry name, finds all the non-lazy module imports in its chain.
- *
- * @return array The array of import names
- */
- private function findEagerEntrypointImports(string $entryName): array
- {
- $dumpedEntrypointPath = $this->assetsPathResolver->getPublicFilesystemPath().'/'.sprintf(self::ENTRYPOINT_CACHE_FILENAME_PATTERN, $entryName);
- if (is_file($dumpedEntrypointPath)) {
- return json_decode(file_get_contents($dumpedEntrypointPath), true, 512, \JSON_THROW_ON_ERROR);
- }
-
- $rootImportEntries = $this->importMapConfigReader->getEntries();
- if (!$rootImportEntries->has($entryName)) {
- throw new \InvalidArgumentException(sprintf('The entrypoint "%s" does not exist in "importmap.php".', $entryName));
- }
-
- if (!$rootImportEntries->get($entryName)->isEntrypoint) {
- throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is not an entry point in "importmap.php". Set "entrypoint" => true to make it available as an entrypoint.', $entryName));
- }
-
- if ($rootImportEntries->get($entryName)->isRemotePackage()) {
- throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is a remote package and cannot be used as an entrypoint.', $entryName));
- }
-
- $asset = $this->findAsset($rootImportEntries->get($entryName)->path);
- if (!$asset) {
- throw new \InvalidArgumentException(sprintf('The path "%s" of the entrypoint "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $rootImportEntries->get($entryName)->path, $entryName));
- }
-
- return $this->findEagerImports($asset);
- }
-
- private function findEagerImports(MappedAsset $asset): array
- {
- $dependencies = [];
- foreach ($asset->getJavaScriptImports() as $javaScriptImport) {
- if ($javaScriptImport->isLazy) {
- continue;
- }
-
- $dependencies[] = $javaScriptImport->importName;
-
- // the import is for a MappedAsset? Follow its imports!
- if ($javaScriptImport->asset) {
- $dependencies = array_merge($dependencies, $this->findEagerImports($javaScriptImport->asset));
- }
- }
-
- return $dependencies;
- }
-
private static function getImportMapTypeFromFilename(string $path): ImportMapType
{
return str_ends_with($path, '.css') ? ImportMapType::CSS : ImportMapType::JS;
@@ -429,13 +219,4 @@ private function findAsset(string $path): ?MappedAsset
return $this->assetMapper->getAssetFromSourcePath($path);
}
-
- private function createMissingImportMapAssetException(ImportMapEntry $entry): \InvalidArgumentException
- {
- if ($entry->isRemotePackage()) {
- throw new LogicException(sprintf('The "%s" vendor asset is missing. Try running the "importmap:install" command.', $entry->importName));
- }
-
- throw new LogicException(sprintf('The asset "%s" cannot be found in any asset map paths.', $entry->path));
- }
}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php
index ca5fe2e6b2888..839bac2b2ef37 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php
@@ -30,7 +30,7 @@ class ImportMapRenderer
private const DEFAULT_ES_MODULE_SHIMS_POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.8.0/dist/es-module-shims.js';
public function __construct(
- private readonly ImportMapManager $importMapManager,
+ private readonly ImportMapGenerator $importMapGenerator,
private readonly ?Packages $assetPackages = null,
private readonly string $charset = 'UTF-8',
private readonly string|false $polyfillImportName = false,
@@ -43,7 +43,7 @@ public function render(string|array $entryPoint, array $attributes = []): string
{
$entryPoint = (array) $entryPoint;
- $importMapData = $this->importMapManager->getImportMapData($entryPoint);
+ $importMapData = $this->importMapGenerator->getImportMapData($entryPoint);
$importMap = [];
$modulePreloads = [];
$cssLinks = [];
diff --git a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
index bf4c79e25bc1c..ba3a7f7e419ff 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
@@ -18,8 +18,8 @@
use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler;
use Symfony\Component\AssetMapper\Exception\CircularAssetsException;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
-use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\AssetMapper\MappedAsset;
@@ -32,8 +32,8 @@ public function testCompile(string $sourceLogicalName, string $input, array $exp
{
$asset = new MappedAsset($sourceLogicalName, 'anything', '/assets/'.$sourceLogicalName);
- $importMapManager = $this->createMock(ImportMapManager::class);
- $importMapManager->expects($this->any())
+ $importMapConfigReader = $this->createMock(ImportMapConfigReader::class);
+ $importMapConfigReader->expects($this->any())
->method('findRootImportMapEntry')
->willReturnCallback(function ($importName) {
return match ($importName) {
@@ -43,7 +43,7 @@ public function testCompile(string $sourceLogicalName, string $input, array $exp
default => null,
};
});
- $compiler = new JavaScriptImportPathCompiler($importMapManager);
+ $compiler = new JavaScriptImportPathCompiler($importMapConfigReader);
// compile - and check that content doesn't change
$this->assertSame($input, $compiler->compile($input, $asset, $this->createAssetMapper()));
$actualImports = [];
@@ -311,7 +311,7 @@ public function testImportPathsCanUpdate(string $sourceLogicalName, string $inpu
->method('getAsset')
->willReturn($importedAsset);
- $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapManager::class));
+ $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class));
$this->assertSame($expectedOutput, $compiler->compile($input, $asset, $assetMapper));
}
@@ -380,7 +380,7 @@ public function testMissingImportMode(string $sourceLogicalName, string $input,
$logger = $this->createMock(LoggerInterface::class);
$compiler = new JavaScriptImportPathCompiler(
- $this->createMock(ImportMapManager::class),
+ $this->createMock(ImportMapConfigReader::class),
AssetCompilerInterface::MISSING_IMPORT_STRICT,
$logger
);
@@ -430,7 +430,7 @@ public function testErrorMessageAvoidsCircularException()
});
$asset = new MappedAsset('htmx.js', '/path/to/app.js');
- $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapManager::class));
+ $compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class));
$content = '//** @type {import("./htmx").HtmxApi} */';
$compiled = $compiler->compile($content, $asset, $assetMapper);
// To form a good exception message, the compiler will check for the
diff --git a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
index d4131ae39c377..fd211dbf1d14d 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
@@ -20,7 +20,7 @@
use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\Factory\MappedAssetFactory;
-use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\MappedAsset;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
@@ -124,7 +124,7 @@ public function testCreateMappedAssetInVendor()
private function createFactory(AssetCompilerInterface $extraCompiler = null): MappedAssetFactory
{
$compilers = [
- new JavaScriptImportPathCompiler($this->createMock(ImportMapManager::class)),
+ new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class)),
new CssAssetUrlCompiler(),
];
if ($extraCompiler) {
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php
index 5cfbf76c5e70b..a70da9dc22aa2 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php
@@ -114,4 +114,12 @@ public function testGetRootDirectory()
$configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php', $this->createMock(RemotePackageStorage::class));
$this->assertSame(__DIR__.'/../fixtures', $configReader->getRootDirectory());
}
+
+ public function testFindRootImportMapEntry()
+ {
+ $configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php', $this->createMock(RemotePackageStorage::class));
+ $entry = $configReader->findRootImportMapEntry('file2');
+ $this->assertSame('file2', $entry->importName);
+ $this->assertSame('file2.js', $entry->path);
+ }
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php
new file mode 100644
index 0000000000000..036d1449b2311
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php
@@ -0,0 +1,769 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\AssetMapper\Tests\ImportMap;
+
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\AssetMapper\AssetMapperInterface;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
+use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport;
+use Symfony\Component\AssetMapper\MappedAsset;
+use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
+use Symfony\Component\Filesystem\Filesystem;
+
+class ImportMapGeneratorTest extends TestCase
+{
+ private AssetMapperInterface&MockObject $assetMapper;
+ private PublicAssetsPathResolverInterface&MockObject $pathResolver;
+ private ImportMapConfigReader&MockObject $configReader;
+ private ImportMapGenerator $importMapGenerator;
+
+ private Filesystem $filesystem;
+ private static string $writableRoot = __DIR__.'/../fixtures/importmaps_for_writing';
+
+ protected function setUp(): void
+ {
+ $this->filesystem = new Filesystem();
+ if (!file_exists(__DIR__.'/../fixtures/importmaps_for_writing')) {
+ $this->filesystem->mkdir(self::$writableRoot);
+ }
+ if (!file_exists(__DIR__.'/../fixtures/importmaps_for_writing/assets')) {
+ $this->filesystem->mkdir(self::$writableRoot.'/assets');
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ $this->filesystem->remove(self::$writableRoot);
+ }
+
+ public function testGetEntrypointNames()
+ {
+ $manager = $this->createImportMapGenerator();
+ $this->mockImportMap([
+ ImportMapEntry::createLocal('entry1', ImportMapType::JS, path: '/any', isEntrypoint: true),
+ ImportMapEntry::createLocal('entry2', ImportMapType::JS, path: '/any', isEntrypoint: true),
+ ImportMapEntry::createLocal('not_entrypoint', ImportMapType::JS, path: '/any', isEntrypoint: false),
+ ]);
+
+ $this->assertEquals(['entry1', 'entry2'], $manager->getEntrypointNames());
+ }
+
+ public function testGetImportMapData()
+ {
+ $manager = $this->createImportMapGenerator();
+ $this->mockImportMap([
+ self::createLocalEntry(
+ 'entry1',
+ path: 'entry1.js',
+ isEntrypoint: true,
+ ),
+ self::createLocalEntry(
+ 'entry2',
+ path: 'entry2.js',
+ isEntrypoint: true,
+ ),
+ self::createLocalEntry(
+ 'entry3',
+ path: 'entry3.js',
+ isEntrypoint: true,
+ ),
+ self::createLocalEntry(
+ 'normal_js_file',
+ path: 'normal_js_file.js',
+ ),
+ self::createLocalEntry(
+ 'css_in_importmap',
+ path: 'styles/css_in_importmap.css',
+ type: ImportMapType::CSS,
+ ),
+ self::createLocalEntry(
+ 'never_imported_css',
+ path: 'styles/never_imported_css.css',
+ type: ImportMapType::CSS,
+ ),
+ ]);
+
+ $importedFile1 = new MappedAsset(
+ 'imported_file1.js',
+ publicPathWithoutDigest: '/assets/imported_file1.js',
+ publicPath: '/assets/imported_file1-d1g35t.js',
+ );
+ $importedFile2 = new MappedAsset(
+ 'imported_file2.js',
+ publicPathWithoutDigest: '/assets/imported_file2.js',
+ publicPath: '/assets/imported_file2-d1g35t.js',
+ );
+ $importedFile3 = new MappedAsset(
+ 'imported_file3.js',
+ publicPathWithoutDigest: '/assets/imported_file3.js',
+ publicPath: '/assets/imported_file3-d1g35t.js',
+ );
+ $normalJsFile = new MappedAsset(
+ 'normal_js_file.js',
+ publicPathWithoutDigest: '/assets/normal_js_file.js',
+ publicPath: '/assets/normal_js_file-d1g35t.js',
+ );
+ $importedCss1 = new MappedAsset(
+ 'styles/file1.css',
+ publicPathWithoutDigest: '/assets/styles/file1.css',
+ publicPath: '/assets/styles/file1-d1g35t.css',
+ );
+ $importedCss2 = new MappedAsset(
+ 'styles/file2.css',
+ publicPathWithoutDigest: '/assets/styles/file2.css',
+ publicPath: '/assets/styles/file2-d1g35t.css',
+ );
+ $importedCssInImportmap = new MappedAsset(
+ 'styles/css_in_importmap.css',
+ publicPathWithoutDigest: '/assets/styles/css_in_importmap.css',
+ publicPath: '/assets/styles/css_in_importmap-d1g35t.css',
+ );
+ $neverImportedCss = new MappedAsset(
+ 'styles/never_imported_css.css',
+ publicPathWithoutDigest: '/assets/styles/never_imported_css.css',
+ publicPath: '/assets/styles/never_imported_css-d1g35t.css',
+ );
+ $this->mockAssetMapper([
+ new MappedAsset(
+ 'entry1.js',
+ publicPath: '/assets/entry1-d1g35t.js',
+ javaScriptImports: [
+ new JavaScriptImport('/assets/imported_file1.js', isLazy: false, asset: $importedFile1, addImplicitlyToImportMap: true),
+ new JavaScriptImport('/assets/styles/file1.css', isLazy: false, asset: $importedCss1, addImplicitlyToImportMap: true),
+ new JavaScriptImport('normal_js_file', isLazy: false, asset: $normalJsFile),
+ ]
+ ),
+ new MappedAsset(
+ 'entry2.js',
+ publicPath: '/assets/entry2-d1g35t.js',
+ javaScriptImports: [
+ new JavaScriptImport('/assets/imported_file2.js', isLazy: false, asset: $importedFile2, addImplicitlyToImportMap: true),
+ new JavaScriptImport('css_in_importmap', isLazy: false, asset: $importedCssInImportmap),
+ new JavaScriptImport('/assets/styles/file2.css', isLazy: false, asset: $importedCss2, addImplicitlyToImportMap: true),
+ ]
+ ),
+ new MappedAsset(
+ 'entry3.js',
+ publicPath: '/assets/entry3-d1g35t.js',
+ javaScriptImports: [
+ new JavaScriptImport('/assets/imported_file3.js', isLazy: false, asset: $importedFile3),
+ ],
+ ),
+ $importedFile1,
+ $importedFile2,
+ // $importedFile3,
+ $normalJsFile,
+ $importedCss1,
+ $importedCss2,
+ $importedCssInImportmap,
+ $neverImportedCss,
+ ]);
+
+ $actualImportMapData = $manager->getImportMapData(['entry2', 'entry1']);
+
+ $this->assertEquals([
+ 'entry1' => [
+ 'path' => '/assets/entry1-d1g35t.js',
+ 'type' => 'js',
+ 'preload' => true, // Rendered entry points are preloaded
+ ],
+ '/assets/imported_file1.js' => [
+ 'path' => '/assets/imported_file1-d1g35t.js',
+ 'type' => 'js',
+ 'preload' => true,
+ ],
+ 'entry2' => [
+ 'path' => '/assets/entry2-d1g35t.js',
+ 'type' => 'js',
+ 'preload' => true, // Rendered entry points are preloaded
+ ],
+ '/assets/imported_file2.js' => [
+ 'path' => '/assets/imported_file2-d1g35t.js',
+ 'type' => 'js',
+ 'preload' => true,
+ ],
+ 'normal_js_file' => [
+ 'path' => '/assets/normal_js_file-d1g35t.js',
+ 'type' => 'js',
+ 'preload' => true, // preloaded as it's a non-lazy dependency of an entry
+ ],
+ '/assets/styles/file1.css' => [
+ 'path' => '/assets/styles/file1-d1g35t.css',
+ 'type' => 'css',
+ 'preload' => true,
+ ],
+ '/assets/styles/file2.css' => [
+ 'path' => '/assets/styles/file2-d1g35t.css',
+ 'type' => 'css',
+ 'preload' => true,
+ ],
+ 'css_in_importmap' => [
+ 'path' => '/assets/styles/css_in_importmap-d1g35t.css',
+ 'type' => 'css',
+ 'preload' => true,
+ ],
+ 'entry3' => [
+ 'path' => '/assets/entry3-d1g35t.js',
+ 'type' => 'js', // No preload (entry point not "rendered")
+ ],
+ 'never_imported_css' => [
+ 'path' => '/assets/styles/never_imported_css-d1g35t.css',
+ 'type' => 'css',
+ ],
+ ], $actualImportMapData);
+
+ // now check the order
+ $this->assertEquals([
+ // entry2 & its dependencies
+ 'entry2',
+ '/assets/imported_file2.js',
+ 'css_in_importmap', // in the importmap, but brought earlier because it's a dependency of entry2
+ '/assets/styles/file2.css',
+
+ // entry1 & its dependencies
+ 'entry1',
+ '/assets/imported_file1.js',
+ '/assets/styles/file1.css',
+ 'normal_js_file',
+
+ // importmap entries never imported
+ 'entry3',
+ 'never_imported_css',
+ ], array_keys($actualImportMapData));
+ }
+
+ /**
+ * @dataProvider getRawImportMapDataTests
+ */
+ public function testGetRawImportMapData(array $importMapEntries, array $mappedAssets, array $expectedData)
+ {
+ $manager = $this->createImportMapGenerator();
+ $this->mockImportMap($importMapEntries);
+ $this->mockAssetMapper($mappedAssets);
+ $this->configReader->expects($this->any())
+ ->method('getRootDirectory')
+ ->willReturn('/fake/root');
+
+ $this->assertEquals($expectedData, $manager->getRawImportMapData());
+ }
+
+ public function getRawImportMapDataTests(): iterable
+ {
+ yield 'it returns remote downloaded entry' => [
+ [
+ self::createRemoteEntry(
+ '@hotwired/stimulus',
+ version: '1.2.3',
+ path: '/assets/vendor/stimulus.js'
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'vendor/@hotwired/stimulus.js',
+ '/assets/vendor/stimulus.js',
+ publicPath: '/assets/vendor/@hotwired/stimulus-d1g35t.js',
+ ),
+ ],
+ [
+ '@hotwired/stimulus' => [
+ 'path' => '/assets/vendor/@hotwired/stimulus-d1g35t.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'it returns basic local javascript file' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: 'app.js'
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app-d13g35t.js',
+ ),
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d13g35t.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'it returns basic local css file' => [
+ [
+ self::createLocalEntry(
+ 'app.css',
+ path: 'styles/app.css',
+ type: ImportMapType::CSS,
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'styles/app.css',
+ publicPath: '/assets/styles/app-d13g35t.css',
+ ),
+ ],
+ [
+ 'app.css' => [
+ 'path' => '/assets/styles/app-d13g35t.css',
+ 'type' => 'css',
+ ],
+ ],
+ ];
+
+ $simpleAsset = new MappedAsset(
+ 'simple.js',
+ publicPathWithoutDigest: '/assets/simple.js',
+ publicPath: '/assets/simple-d1g3st.js',
+ );
+ yield 'it adds dependency to the importmap' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: 'app.js',
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app-d1g3st.js',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
+ ),
+ $simpleAsset,
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d1g3st.js',
+ 'type' => 'js',
+ ],
+ '/assets/simple.js' => [
+ 'path' => '/assets/simple-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'it adds dependency to the importmap from a remote asset' => [
+ [
+ self::createRemoteEntry(
+ 'bootstrap',
+ version: '1.2.3',
+ path: '/assets/vendor/bootstrap.js'
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ sourcePath: '/assets/vendor/bootstrap.js',
+ publicPath: '/assets/vendor/bootstrap-d1g3st.js',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
+ ),
+ $simpleAsset,
+ ],
+ [
+ 'bootstrap' => [
+ 'path' => '/assets/vendor/bootstrap-d1g3st.js',
+ 'type' => 'js',
+ ],
+ '/assets/simple.js' => [
+ 'path' => '/assets/simple-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ $eagerImportsSimpleAsset = new MappedAsset(
+ 'imports_simple.js',
+ publicPathWithoutDigest: '/assets/imports_simple.js',
+ publicPath: '/assets/imports_simple-d1g3st.js',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
+ );
+ yield 'it processes imports recursively' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: 'app.js',
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app-d1g3st.js',
+ javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: true)]
+ ),
+ $eagerImportsSimpleAsset,
+ $simpleAsset,
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d1g3st.js',
+ 'type' => 'js',
+ ],
+ '/assets/imports_simple.js' => [
+ 'path' => '/assets/imports_simple-d1g3st.js',
+ 'type' => 'js',
+ ],
+ '/assets/simple.js' => [
+ 'path' => '/assets/simple-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'it process can skip adding one importmap entry but still add a child' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: 'app.js',
+ ),
+ self::createLocalEntry(
+ 'imports_simple',
+ path: 'imports_simple.js',
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app-d1g3st.js',
+ javaScriptImports: [new JavaScriptImport('imports_simple', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: false)]
+ ),
+ $eagerImportsSimpleAsset,
+ $simpleAsset,
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d1g3st.js',
+ 'type' => 'js',
+ ],
+ '/assets/simple.js' => [
+ 'path' => '/assets/simple-d1g3st.js',
+ 'type' => 'js',
+ ],
+ 'imports_simple' => [
+ 'path' => '/assets/imports_simple-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'imports with a module name are not added to the importmap' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: 'app.js',
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app-d1g3st.js',
+ javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)]
+ ),
+ $simpleAsset,
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'it does not process dependencies of CSS files' => [
+ [
+ self::createLocalEntry(
+ 'app.css',
+ path: 'app.css',
+ type: ImportMapType::CSS,
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.css',
+ publicPath: '/assets/app-d1g3st.css',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', asset: $simpleAsset)]
+ ),
+ ],
+ [
+ 'app.css' => [
+ 'path' => '/assets/app-d1g3st.css',
+ 'type' => 'css',
+ ],
+ ],
+ ];
+
+ yield 'it handles a relative path file' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: './assets/app.js',
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ // /fake/root is the mocked root directory
+ '/fake/root/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
+ ),
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+
+ yield 'it handles an absolute path file' => [
+ [
+ self::createLocalEntry(
+ 'app',
+ path: '/some/path/assets/app.js',
+ ),
+ ],
+ [
+ new MappedAsset(
+ 'app.js',
+ '/some/path/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
+ ),
+ ],
+ [
+ 'app' => [
+ 'path' => '/assets/app-d1g3st.js',
+ 'type' => 'js',
+ ],
+ ],
+ ];
+ }
+
+ public function testGetRawImportDataUsesCacheFile()
+ {
+ $manager = $this->createImportMapGenerator();
+ $importmapData = [
+ 'app' => [
+ 'path' => 'app.js',
+ 'entrypoint' => true,
+ ],
+ '@hotwired/stimulus' => [
+ 'path' => 'https://anyurl.com/stimulus',
+ ],
+ ];
+ $this->writeFile('public/assets/importmap.json', json_encode($importmapData));
+ $this->pathResolver->expects($this->once())
+ ->method('getPublicFilesystemPath')
+ ->willReturn(self::$writableRoot.'/public/assets');
+
+ $this->assertEquals($importmapData, $manager->getRawImportMapData());
+ }
+
+ /**
+ * @dataProvider getEagerEntrypointImportsTests
+ */
+ public function testFindEagerEntrypointImports(MappedAsset $entryAsset, array $expected)
+ {
+ $manager = $this->createImportMapGenerator();
+ $this->mockAssetMapper([$entryAsset]);
+ // put the entry asset in the importmap
+ $this->mockImportMap([
+ ImportMapEntry::createLocal('the_entrypoint_name', ImportMapType::JS, path: $entryAsset->logicalPath, isEntrypoint: true),
+ ]);
+
+ $this->assertEquals($expected, $manager->findEagerEntrypointImports('the_entrypoint_name'));
+ }
+
+ public function getEagerEntrypointImportsTests(): iterable
+ {
+ yield 'an entry with no dependencies' => [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app.js',
+ ),
+ [],
+ ];
+
+ $simpleAsset = new MappedAsset(
+ 'simple.js',
+ publicPathWithoutDigest: '/assets/simple.js',
+ );
+ yield 'an entry with a non-lazy dependency is included' => [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app.js',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)]
+ ),
+ ['/assets/simple.js'], // path is the key in the importmap
+ ];
+
+ yield 'an entry with a non-lazy dependency with module name is included' => [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app.js',
+ javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)]
+ ),
+ ['simple'], // path is the key in the importmap
+ ];
+
+ yield 'an entry with a lazy dependency is not included' => [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app.js',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: true, asset: $simpleAsset)]
+ ),
+ [],
+ ];
+
+ $importsSimpleAsset = new MappedAsset(
+ 'imports_simple.js',
+ publicPathWithoutDigest: '/assets/imports_simple.js',
+ javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)]
+ );
+ yield 'an entry follows through dependencies recursively' => [
+ new MappedAsset(
+ 'app.js',
+ publicPath: '/assets/app.js',
+ javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: false, asset: $importsSimpleAsset)]
+ ),
+ ['/assets/imports_simple.js', '/assets/simple.js'],
+ ];
+ }
+
+ public function testFindEagerEntrypointImportsUsesCacheFile()
+ {
+ $manager = $this->createImportMapGenerator();
+ $entrypointData = [
+ 'app',
+ '/assets/foo.js',
+ ];
+ $this->writeFile('public/assets/entrypoint.foo.json', json_encode($entrypointData));
+ $this->pathResolver->expects($this->once())
+ ->method('getPublicFilesystemPath')
+ ->willReturn(self::$writableRoot.'/public/assets');
+
+ $this->assertEquals($entrypointData, $manager->findEagerEntrypointImports('foo'));
+ }
+
+ private function createImportMapGenerator(): ImportMapGenerator
+ {
+ $this->pathResolver = $this->createMock(PublicAssetsPathResolverInterface::class);
+ $this->assetMapper = $this->createMock(AssetMapperInterface::class);
+ $this->configReader = $this->createMock(ImportMapConfigReader::class);
+
+ // mock this to behave like normal
+ $this->configReader->expects($this->any())
+ ->method('createRemoteEntry')
+ ->willReturnCallback(function (string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint) {
+ $path = '/path/to/vendor/'.$packageModuleSpecifier.'.js';
+
+ return ImportMapEntry::createRemote($importName, $type, $path, $version, $packageModuleSpecifier, $isEntrypoint);
+ });
+
+ return $this->importMapGenerator = new ImportMapGenerator(
+ $this->assetMapper,
+ $this->pathResolver,
+ $this->configReader,
+ );
+ }
+
+ private function mockImportMap(array $importMapEntries): void
+ {
+ $this->configReader->expects($this->any())
+ ->method('getEntries')
+ ->willReturn(new ImportMapEntries($importMapEntries))
+ ;
+ }
+
+ private function writeFile(string $filename, string $content): void
+ {
+ $path = \dirname(self::$writableRoot.'/'.$filename);
+ if (!is_dir($path)) {
+ mkdir($path, 0777, true);
+ }
+ file_put_contents(self::$writableRoot.'/'.$filename, $content);
+ }
+
+ private static function createLocalEntry(string $importName, string $path, ImportMapType $type = ImportMapType::JS, bool $isEntrypoint = false): ImportMapEntry
+ {
+ return ImportMapEntry::createLocal($importName, $type, path: $path, isEntrypoint: $isEntrypoint);
+ }
+
+ private static function createRemoteEntry(string $importName, string $version, string $path = null, ImportMapType $type = ImportMapType::JS, string $packageSpecifier = null): ImportMapEntry
+ {
+ $packageSpecifier = $packageSpecifier ?? $importName;
+ $path = $path ?? '/vendor/any-path.js';
+
+ return ImportMapEntry::createRemote($importName, $type, path: $path, version: $version, packageModuleSpecifier: $packageSpecifier, isEntrypoint: false);
+ }
+
+ /**
+ * @param MappedAsset[] $mappedAssets
+ */
+ private function mockAssetMapper(array $mappedAssets): void
+ {
+ $this->assetMapper->expects($this->any())
+ ->method('getAsset')
+ ->willReturnCallback(function (string $logicalPath) use ($mappedAssets) {
+ foreach ($mappedAssets as $asset) {
+ if ($asset->logicalPath === $logicalPath) {
+ return $asset;
+ }
+ }
+
+ return null;
+ })
+ ;
+
+ $this->assetMapper->expects($this->any())
+ ->method('getAssetFromSourcePath')
+ ->willReturnCallback(function (string $sourcePath) use ($mappedAssets) {
+ // collapse ../ in paths and ./ in paths to mimic the realpath AssetMapper uses
+ $unCollapsePath = function (string $path) {
+ $parts = explode('/', $path);
+ $newParts = [];
+ foreach ($parts as $part) {
+ if ('..' === $part) {
+ array_pop($newParts);
+
+ continue;
+ }
+
+ if ('.' !== $part) {
+ $newParts[] = $part;
+ }
+ }
+
+ return implode('/', $newParts);
+ };
+
+ $sourcePath = $unCollapsePath($sourcePath);
+
+ foreach ($mappedAssets as $asset) {
+ if (isset($asset->sourcePath) && $unCollapsePath($asset->sourcePath) === $sourcePath) {
+ return $asset;
+ }
+ }
+
+ return null;
+ })
+ ;
+ }
+}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
index 6c42c6df051e3..357350aef9c7b 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
@@ -19,19 +19,16 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
-use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport;
use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions;
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface;
use Symfony\Component\AssetMapper\ImportMap\Resolver\ResolvedImportMapPackage;
use Symfony\Component\AssetMapper\MappedAsset;
-use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
use Symfony\Component\Filesystem\Filesystem;
class ImportMapManagerTest extends TestCase
{
private AssetMapperInterface&MockObject $assetMapper;
- private PublicAssetsPathResolverInterface&MockObject $pathResolver;
private PackageResolverInterface&MockObject $packageResolver;
private ImportMapConfigReader&MockObject $configReader;
private RemotePackageDownloader&MockObject $remotePackageDownloader;
@@ -56,628 +53,6 @@ protected function tearDown(): void
$this->filesystem->remove(self::$writableRoot);
}
- /**
- * @dataProvider getRawImportMapDataTests
- */
- public function testGetRawImportMapData(array $importMapEntries, array $mappedAssets, array $expectedData)
- {
- $manager = $this->createImportMapManager();
- $this->mockImportMap($importMapEntries);
- $this->mockAssetMapper($mappedAssets);
- $this->configReader->expects($this->any())
- ->method('getRootDirectory')
- ->willReturn('/fake/root');
-
- $this->assertEquals($expectedData, $manager->getRawImportMapData());
- }
-
- public function getRawImportMapDataTests(): iterable
- {
- yield 'it returns remote downloaded entry' => [
- [
- self::createRemoteEntry(
- '@hotwired/stimulus',
- version: '1.2.3',
- path: '/assets/vendor/stimulus.js'
- ),
- ],
- [
- new MappedAsset(
- 'vendor/@hotwired/stimulus.js',
- '/assets/vendor/stimulus.js',
- publicPath: '/assets/vendor/@hotwired/stimulus-d1g35t.js',
- ),
- ],
- [
- '@hotwired/stimulus' => [
- 'path' => '/assets/vendor/@hotwired/stimulus-d1g35t.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'it returns basic local javascript file' => [
- [
- self::createLocalEntry(
- 'app',
- path: 'app.js'
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app-d13g35t.js',
- ),
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d13g35t.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'it returns basic local css file' => [
- [
- self::createLocalEntry(
- 'app.css',
- path: 'styles/app.css',
- type: ImportMapType::CSS,
- ),
- ],
- [
- new MappedAsset(
- 'styles/app.css',
- publicPath: '/assets/styles/app-d13g35t.css',
- ),
- ],
- [
- 'app.css' => [
- 'path' => '/assets/styles/app-d13g35t.css',
- 'type' => 'css',
- ],
- ],
- ];
-
- $simpleAsset = new MappedAsset(
- 'simple.js',
- publicPathWithoutDigest: '/assets/simple.js',
- publicPath: '/assets/simple-d1g3st.js',
- );
- yield 'it adds dependency to the importmap' => [
- [
- self::createLocalEntry(
- 'app',
- path: 'app.js',
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app-d1g3st.js',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
- ),
- $simpleAsset,
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d1g3st.js',
- 'type' => 'js',
- ],
- '/assets/simple.js' => [
- 'path' => '/assets/simple-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'it adds dependency to the importmap from a remote asset' => [
- [
- self::createRemoteEntry(
- 'bootstrap',
- version: '1.2.3',
- path: '/assets/vendor/bootstrap.js'
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- sourcePath: '/assets/vendor/bootstrap.js',
- publicPath: '/assets/vendor/bootstrap-d1g3st.js',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
- ),
- $simpleAsset,
- ],
- [
- 'bootstrap' => [
- 'path' => '/assets/vendor/bootstrap-d1g3st.js',
- 'type' => 'js',
- ],
- '/assets/simple.js' => [
- 'path' => '/assets/simple-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- $eagerImportsSimpleAsset = new MappedAsset(
- 'imports_simple.js',
- publicPathWithoutDigest: '/assets/imports_simple.js',
- publicPath: '/assets/imports_simple-d1g3st.js',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
- );
- yield 'it processes imports recursively' => [
- [
- self::createLocalEntry(
- 'app',
- path: 'app.js',
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app-d1g3st.js',
- javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: true)]
- ),
- $eagerImportsSimpleAsset,
- $simpleAsset,
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d1g3st.js',
- 'type' => 'js',
- ],
- '/assets/imports_simple.js' => [
- 'path' => '/assets/imports_simple-d1g3st.js',
- 'type' => 'js',
- ],
- '/assets/simple.js' => [
- 'path' => '/assets/simple-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'it process can skip adding one importmap entry but still add a child' => [
- [
- self::createLocalEntry(
- 'app',
- path: 'app.js',
- ),
- self::createLocalEntry(
- 'imports_simple',
- path: 'imports_simple.js',
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app-d1g3st.js',
- javaScriptImports: [new JavaScriptImport('imports_simple', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: false)]
- ),
- $eagerImportsSimpleAsset,
- $simpleAsset,
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d1g3st.js',
- 'type' => 'js',
- ],
- '/assets/simple.js' => [
- 'path' => '/assets/simple-d1g3st.js',
- 'type' => 'js',
- ],
- 'imports_simple' => [
- 'path' => '/assets/imports_simple-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'imports with a module name are not added to the importmap' => [
- [
- self::createLocalEntry(
- 'app',
- path: 'app.js',
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app-d1g3st.js',
- javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)]
- ),
- $simpleAsset,
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'it does not process dependencies of CSS files' => [
- [
- self::createLocalEntry(
- 'app.css',
- path: 'app.css',
- type: ImportMapType::CSS,
- ),
- ],
- [
- new MappedAsset(
- 'app.css',
- publicPath: '/assets/app-d1g3st.css',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', asset: $simpleAsset)]
- ),
- ],
- [
- 'app.css' => [
- 'path' => '/assets/app-d1g3st.css',
- 'type' => 'css',
- ],
- ],
- ];
-
- yield 'it handles a relative path file' => [
- [
- self::createLocalEntry(
- 'app',
- path: './assets/app.js',
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- // /fake/root is the mocked root directory
- '/fake/root/assets/app.js',
- publicPath: '/assets/app-d1g3st.js',
- ),
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
-
- yield 'it handles an absolute path file' => [
- [
- self::createLocalEntry(
- 'app',
- path: '/some/path/assets/app.js',
- ),
- ],
- [
- new MappedAsset(
- 'app.js',
- '/some/path/assets/app.js',
- publicPath: '/assets/app-d1g3st.js',
- ),
- ],
- [
- 'app' => [
- 'path' => '/assets/app-d1g3st.js',
- 'type' => 'js',
- ],
- ],
- ];
- }
-
- public function testGetRawImportDataUsesCacheFile()
- {
- $manager = $this->createImportMapManager();
- $importmapData = [
- 'app' => [
- 'path' => 'app.js',
- 'entrypoint' => true,
- ],
- '@hotwired/stimulus' => [
- 'path' => 'https://anyurl.com/stimulus',
- ],
- ];
- $this->writeFile('public/assets/importmap.json', json_encode($importmapData));
- $this->pathResolver->expects($this->once())
- ->method('getPublicFilesystemPath')
- ->willReturn(self::$writableRoot.'/public/assets');
-
- $this->assertEquals($importmapData, $manager->getRawImportMapData());
- }
-
- /**
- * @dataProvider getEntrypointMetadataTests
- */
- public function testGetEntrypointMetadata(MappedAsset $entryAsset, array $expected)
- {
- $manager = $this->createImportMapManager();
- $this->mockAssetMapper([$entryAsset]);
- // put the entry asset in the importmap
- $this->mockImportMap([
- ImportMapEntry::createLocal('the_entrypoint_name', ImportMapType::JS, path: $entryAsset->logicalPath, isEntrypoint: true),
- ]);
-
- $this->assertEquals($expected, $manager->getEntrypointMetadata('the_entrypoint_name'));
- }
-
- public function getEntrypointMetadataTests(): iterable
- {
- yield 'an entry with no dependencies' => [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app.js',
- ),
- [],
- ];
-
- $simpleAsset = new MappedAsset(
- 'simple.js',
- publicPathWithoutDigest: '/assets/simple.js',
- );
- yield 'an entry with a non-lazy dependency is included' => [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app.js',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)]
- ),
- ['/assets/simple.js'], // path is the key in the importmap
- ];
-
- yield 'an entry with a non-lazy dependency with module name is included' => [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app.js',
- javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)]
- ),
- ['simple'], // path is the key in the importmap
- ];
-
- yield 'an entry with a lazy dependency is not included' => [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app.js',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: true, asset: $simpleAsset)]
- ),
- [],
- ];
-
- $importsSimpleAsset = new MappedAsset(
- 'imports_simple.js',
- publicPathWithoutDigest: '/assets/imports_simple.js',
- javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset)]
- );
- yield 'an entry follows through dependencies recursively' => [
- new MappedAsset(
- 'app.js',
- publicPath: '/assets/app.js',
- javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: false, asset: $importsSimpleAsset)]
- ),
- ['/assets/imports_simple.js', '/assets/simple.js'],
- ];
- }
-
- public function testGetEntrypointMetadataUsesCacheFile()
- {
- $manager = $this->createImportMapManager();
- $entrypointData = [
- 'app',
- '/assets/foo.js',
- ];
- $this->writeFile('public/assets/entrypoint.foo.json', json_encode($entrypointData));
- $this->pathResolver->expects($this->once())
- ->method('getPublicFilesystemPath')
- ->willReturn(self::$writableRoot.'/public/assets');
-
- $this->assertEquals($entrypointData, $manager->getEntrypointMetadata('foo'));
- }
-
- public function testGetImportMapData()
- {
- $manager = $this->createImportMapManager();
- $this->mockImportMap([
- self::createLocalEntry(
- 'entry1',
- path: 'entry1.js',
- isEntrypoint: true,
- ),
- self::createLocalEntry(
- 'entry2',
- path: 'entry2.js',
- isEntrypoint: true,
- ),
- self::createLocalEntry(
- 'entry3',
- path: 'entry3.js',
- isEntrypoint: true,
- ),
- self::createLocalEntry(
- 'normal_js_file',
- path: 'normal_js_file.js',
- ),
- self::createLocalEntry(
- 'css_in_importmap',
- path: 'styles/css_in_importmap.css',
- type: ImportMapType::CSS,
- ),
- self::createLocalEntry(
- 'never_imported_css',
- path: 'styles/never_imported_css.css',
- type: ImportMapType::CSS,
- ),
- ]);
-
- $importedFile1 = new MappedAsset(
- 'imported_file1.js',
- publicPathWithoutDigest: '/assets/imported_file1.js',
- publicPath: '/assets/imported_file1-d1g35t.js',
- );
- $importedFile2 = new MappedAsset(
- 'imported_file2.js',
- publicPathWithoutDigest: '/assets/imported_file2.js',
- publicPath: '/assets/imported_file2-d1g35t.js',
- );
- $importedFile3 = new MappedAsset(
- 'imported_file3.js',
- publicPathWithoutDigest: '/assets/imported_file3.js',
- publicPath: '/assets/imported_file3-d1g35t.js',
- );
- $normalJsFile = new MappedAsset(
- 'normal_js_file.js',
- publicPathWithoutDigest: '/assets/normal_js_file.js',
- publicPath: '/assets/normal_js_file-d1g35t.js',
- );
- $importedCss1 = new MappedAsset(
- 'styles/file1.css',
- publicPathWithoutDigest: '/assets/styles/file1.css',
- publicPath: '/assets/styles/file1-d1g35t.css',
- );
- $importedCss2 = new MappedAsset(
- 'styles/file2.css',
- publicPathWithoutDigest: '/assets/styles/file2.css',
- publicPath: '/assets/styles/file2-d1g35t.css',
- );
- $importedCssInImportmap = new MappedAsset(
- 'styles/css_in_importmap.css',
- publicPathWithoutDigest: '/assets/styles/css_in_importmap.css',
- publicPath: '/assets/styles/css_in_importmap-d1g35t.css',
- );
- $neverImportedCss = new MappedAsset(
- 'styles/never_imported_css.css',
- publicPathWithoutDigest: '/assets/styles/never_imported_css.css',
- publicPath: '/assets/styles/never_imported_css-d1g35t.css',
- );
- $this->mockAssetMapper([
- new MappedAsset(
- 'entry1.js',
- publicPath: '/assets/entry1-d1g35t.js',
- javaScriptImports: [
- new JavaScriptImport('/assets/imported_file1.js', isLazy: false, asset: $importedFile1, addImplicitlyToImportMap: true),
- new JavaScriptImport('/assets/styles/file1.css', isLazy: false, asset: $importedCss1, addImplicitlyToImportMap: true),
- new JavaScriptImport('normal_js_file', isLazy: false, asset: $normalJsFile),
- ]
- ),
- new MappedAsset(
- 'entry2.js',
- publicPath: '/assets/entry2-d1g35t.js',
- javaScriptImports: [
- new JavaScriptImport('/assets/imported_file2.js', isLazy: false, asset: $importedFile2, addImplicitlyToImportMap: true),
- new JavaScriptImport('css_in_importmap', isLazy: false, asset: $importedCssInImportmap),
- new JavaScriptImport('/assets/styles/file2.css', isLazy: false, asset: $importedCss2, addImplicitlyToImportMap: true),
- ]
- ),
- new MappedAsset(
- 'entry3.js',
- publicPath: '/assets/entry3-d1g35t.js',
- javaScriptImports: [
- new JavaScriptImport('/assets/imported_file3.js', isLazy: false, asset: $importedFile3),
- ],
- ),
- $importedFile1,
- $importedFile2,
- // $importedFile3,
- $normalJsFile,
- $importedCss1,
- $importedCss2,
- $importedCssInImportmap,
- $neverImportedCss,
- ]);
-
- $actualImportMapData = $manager->getImportMapData(['entry2', 'entry1']);
-
- $this->assertEquals([
- 'entry1' => [
- 'path' => '/assets/entry1-d1g35t.js',
- 'type' => 'js',
- 'preload' => true, // Rendered entry points are preloaded
- ],
- '/assets/imported_file1.js' => [
- 'path' => '/assets/imported_file1-d1g35t.js',
- 'type' => 'js',
- 'preload' => true,
- ],
- 'entry2' => [
- 'path' => '/assets/entry2-d1g35t.js',
- 'type' => 'js',
- 'preload' => true, // Rendered entry points are preloaded
- ],
- '/assets/imported_file2.js' => [
- 'path' => '/assets/imported_file2-d1g35t.js',
- 'type' => 'js',
- 'preload' => true,
- ],
- 'normal_js_file' => [
- 'path' => '/assets/normal_js_file-d1g35t.js',
- 'type' => 'js',
- 'preload' => true, // preloaded as it's a non-lazy dependency of an entry
- ],
- '/assets/styles/file1.css' => [
- 'path' => '/assets/styles/file1-d1g35t.css',
- 'type' => 'css',
- 'preload' => true,
- ],
- '/assets/styles/file2.css' => [
- 'path' => '/assets/styles/file2-d1g35t.css',
- 'type' => 'css',
- 'preload' => true,
- ],
- 'css_in_importmap' => [
- 'path' => '/assets/styles/css_in_importmap-d1g35t.css',
- 'type' => 'css',
- 'preload' => true,
- ],
- 'entry3' => [
- 'path' => '/assets/entry3-d1g35t.js',
- 'type' => 'js', // No preload (entry point not "rendered")
- ],
- 'never_imported_css' => [
- 'path' => '/assets/styles/never_imported_css-d1g35t.css',
- 'type' => 'css',
- ],
- ], $actualImportMapData);
-
- // now check the order
- $this->assertEquals([
- // entry2 & its dependencies
- 'entry2',
- '/assets/imported_file2.js',
- 'css_in_importmap', // in the importmap, but brought earlier because it's a dependency of entry2
- '/assets/styles/file2.css',
-
- // entry1 & its dependencies
- 'entry1',
- '/assets/imported_file1.js',
- '/assets/styles/file1.css',
- 'normal_js_file',
-
- // importmap entries never imported
- 'entry3',
- 'never_imported_css',
- ], array_keys($actualImportMapData));
- }
-
- public function testFindRootImportMapEntry()
- {
- $manager = $this->createImportMapManager();
- $entry1 = ImportMapEntry::createLocal('entry1', ImportMapType::JS, '/any/path', isEntrypoint: true);
- $this->mockImportMap([$entry1]);
-
- $this->assertSame($entry1, $manager->findRootImportMapEntry('entry1'));
- $this->assertNull($manager->findRootImportMapEntry('entry2'));
- }
-
- public function testGetEntrypointNames()
- {
- $manager = $this->createImportMapManager();
- $this->mockImportMap([
- ImportMapEntry::createLocal('entry1', ImportMapType::JS, path: '/any', isEntrypoint: true),
- ImportMapEntry::createLocal('entry2', ImportMapType::JS, path: '/any', isEntrypoint: true),
- ImportMapEntry::createLocal('not_entrypoint', ImportMapType::JS, path: '/any', isEntrypoint: false),
- ]);
-
- $this->assertEquals(['entry1', 'entry2'], $manager->getEntrypointNames());
- }
-
/**
* @dataProvider getRequirePackageTests
*/
@@ -987,7 +362,6 @@ public static function getPackageNameTests(): iterable
private function createImportMapManager(): ImportMapManager
{
- $this->pathResolver = $this->createMock(PublicAssetsPathResolverInterface::class);
$this->assetMapper = $this->createMock(AssetMapperInterface::class);
$this->configReader = $this->createMock(ImportMapConfigReader::class);
$this->packageResolver = $this->createMock(PackageResolverInterface::class);
@@ -1004,7 +378,6 @@ private function createImportMapManager(): ImportMapManager
return $this->importMapManager = new ImportMapManager(
$this->assetMapper,
- $this->pathResolver,
$this->configReader,
$this->remotePackageDownloader,
$this->packageResolver,
@@ -1028,59 +401,6 @@ private function mockImportMap(array $importMapEntries): void
;
}
- /**
- * @param MappedAsset[] $mappedAssets
- */
- private function mockAssetMapper(array $mappedAssets): void
- {
- $this->assetMapper->expects($this->any())
- ->method('getAsset')
- ->willReturnCallback(function (string $logicalPath) use ($mappedAssets) {
- foreach ($mappedAssets as $asset) {
- if ($asset->logicalPath === $logicalPath) {
- return $asset;
- }
- }
-
- return null;
- })
- ;
-
- $this->assetMapper->expects($this->any())
- ->method('getAssetFromSourcePath')
- ->willReturnCallback(function (string $sourcePath) use ($mappedAssets) {
- // collapse ../ in paths and ./ in paths to mimic the realpath AssetMapper uses
- $unCollapsePath = function (string $path) {
- $parts = explode('/', $path);
- $newParts = [];
- foreach ($parts as $part) {
- if ('..' === $part) {
- array_pop($newParts);
-
- continue;
- }
-
- if ('.' !== $part) {
- $newParts[] = $part;
- }
- }
-
- return implode('/', $newParts);
- };
-
- $sourcePath = $unCollapsePath($sourcePath);
-
- foreach ($mappedAssets as $asset) {
- if (isset($asset->sourcePath) && $unCollapsePath($asset->sourcePath) === $sourcePath) {
- return $asset;
- }
- }
-
- return null;
- })
- ;
- }
-
private function writeFile(string $filename, string $content): void
{
$path = \dirname(self::$writableRoot.'/'.$filename);
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php
index 3d729d8c8caf7..a0d90e0cc5c15 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapRendererTest.php
@@ -13,7 +13,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Asset\Packages;
-use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator;
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
@@ -23,8 +23,8 @@ class ImportMapRendererTest extends TestCase
{
public function testBasicRender()
{
- $importMapManager = $this->createMock(ImportMapManager::class);
- $importMapManager->expects($this->once())
+ $importMapGenerator = $this->createMock(ImportMapGenerator::class);
+ $importMapGenerator->expects($this->once())
->method('getImportMapData')
->with(['app'])
->willReturn([
@@ -68,7 +68,7 @@ public function testBasicRender()
return '/subdirectory/'.$path;
});
- $renderer = new ImportMapRenderer($importMapManager, $assetPackages, polyfillImportName: 'es-module-shim');
+ $renderer = new ImportMapRenderer($importMapGenerator, $assetPackages, polyfillImportName: 'es-module-shim');
$html = $renderer->render(['app']);
$this->assertStringContainsString('", $renderer->render('application'));
- $renderer = new ImportMapRenderer($this->createBasicImportMapManager());
+ $renderer = new ImportMapRenderer($this->createBasicImportMapGenerator());
$this->assertStringContainsString("", $renderer->render("application's"));
- $renderer = new ImportMapRenderer($this->createBasicImportMapManager());
+ $renderer = new ImportMapRenderer($this->createBasicImportMapGenerator());
$html = $renderer->render(['foo', 'bar']);
$this->assertStringContainsString("import 'foo';", $html);
$this->assertStringContainsString("import 'bar';", $html);
}
- private function createBasicImportMapManager(): ImportMapManager
+ private function createBasicImportMapGenerator(): ImportMapGenerator
{
- $importMapManager = $this->createMock(ImportMapManager::class);
- $importMapManager->expects($this->once())
+ $importMapGenerator = $this->createMock(ImportMapGenerator::class);
+ $importMapGenerator->expects($this->once())
->method('getImportMapData')
->willReturn([
'app' => [
@@ -159,13 +159,13 @@ private function createBasicImportMapManager(): ImportMapManager
])
;
- return $importMapManager;
+ return $importMapGenerator;
}
public function testItAddsPreloadLinks()
{
- $importMapManager = $this->createMock(ImportMapManager::class);
- $importMapManager->expects($this->once())
+ $importMapGenerator = $this->createMock(ImportMapGenerator::class);
+ $importMapGenerator->expects($this->once())
->method('getImportMapData')
->willReturn([
'app_js_preload' => [
@@ -188,7 +188,7 @@ public function testItAddsPreloadLinks()
$requestStack = new RequestStack();
$requestStack->push($request);
- $renderer = new ImportMapRenderer($importMapManager, requestStack: $requestStack);
+ $renderer = new ImportMapRenderer($importMapGenerator, requestStack: $requestStack);
$renderer->render(['app']);
$linkProvider = $request->attributes->get('_links');