diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 7592ed38d2e48..ae342f1c6b918 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -1363,8 +1363,8 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
->setArgument(1, $config['missing_import_mode']);
$container
- ->getDefinition('asset_mapper.importmap.remote_package_downloader')
- ->replaceArgument(2, $config['vendor_dir'])
+ ->getDefinition('asset_mapper.importmap.remote_package_storage')
+ ->replaceArgument(0, $config['vendor_dir'])
;
$container
->getDefinition('asset_mapper.mapped_asset_factory')
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
index 296358cfcf72c..2a3ca8b6e9887 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
@@ -35,6 +35,7 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
+use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
use Symfony\Component\AssetMapper\MapperAwareAssetPackage;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver;
@@ -145,6 +146,7 @@
->set('asset_mapper.importmap.config_reader', ImportMapConfigReader::class)
->args([
abstract_arg('importmap.php path'),
+ service('asset_mapper.importmap.remote_package_storage'),
])
->set('asset_mapper.importmap.manager', ImportMapManager::class)
@@ -157,11 +159,16 @@
])
->alias(ImportMapManager::class, 'asset_mapper.importmap.manager')
+ ->set('asset_mapper.importmap.remote_package_storage', RemotePackageStorage::class)
+ ->args([
+ abstract_arg('vendor directory'),
+ ])
+
->set('asset_mapper.importmap.remote_package_downloader', RemotePackageDownloader::class)
->args([
+ service('asset_mapper.importmap.remote_package_storage'),
service('asset_mapper.importmap.config_reader'),
service('asset_mapper.importmap.resolver'),
- abstract_arg('vendor directory'),
])
->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class)
diff --git a/src/Symfony/Component/AssetMapper/Command/ImportMapOutdatedCommand.php b/src/Symfony/Component/AssetMapper/Command/ImportMapOutdatedCommand.php
index 2f1c6d64d5353..ac188a009520a 100644
--- a/src/Symfony/Component/AssetMapper/Command/ImportMapOutdatedCommand.php
+++ b/src/Symfony/Component/AssetMapper/Command/ImportMapOutdatedCommand.php
@@ -73,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return Command::SUCCESS;
}
- $displayData = array_map(fn ($importName, $packageUpdateInfo) => [
+ $displayData = array_map(fn (string $importName, PackageUpdateInfo $packageUpdateInfo) => [
'name' => $importName,
'current' => $packageUpdateInfo->currentVersion,
'latest' => $packageUpdateInfo->latestVersion,
diff --git a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php
index 3f297039e81f9..6a5fb54e2781a 100644
--- a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php
+++ b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php
@@ -54,14 +54,10 @@ protected function configure(): void
php %command.full_name% "chart.js/auto"
-Or download one package/file, but alias its name in your import map:
+Or require one package/file, but alias its name in your import map:
php %command.full_name% "vue/dist/vue.esm-bundler.js=vue"
-The download option will download the package locally and point the
-importmap to it. Use this if you want to avoid using a CDN or if you want to
-ensure that the package is available even if the CDN is down.
-
Sometimes, a package may require other packages and multiple new items may be added
to the import map.
@@ -69,6 +65,10 @@ protected function configure(): void
php %command.full_name% "lodash@^4.15" "@hotwired/stimulus"
+To add an importmap entry pointing to a local file, use the path option:
+
+ php %command.full_name% "any_module_name" --path=./assets/some_file.js
+
EOT
);
}
@@ -87,15 +87,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
$path = $input->getOption('path');
- if (!is_file($path)) {
- $path = $this->projectDir.'/'.$path;
-
- if (!is_file($path)) {
- $io->error(sprintf('The path "%s" does not exist.', $input->getOption('path')));
-
- return Command::FAILURE;
- }
- }
}
$packages = [];
@@ -110,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$packages[] = new PackageRequireOptions(
$parts['package'],
$parts['version'] ?? null,
- $parts['alias'] ?? $parts['package'],
+ $parts['alias'] ?? null,
$path,
$input->getOption('entrypoint'),
);
diff --git a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
index 147d63a40b82c..f1f33d71eedc0 100644
--- a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
+++ b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php
@@ -28,8 +28,8 @@ final class JavaScriptImportPathCompiler implements AssetCompilerInterface
{
use AssetCompilerPathResolverTrait;
- // https://regex101.com/r/5Q38tj/1
- private const IMPORT_PATTERN = '/(?:import\s+(?:(?:\*\s+as\s+\w+|[\w\s{},*]+)\s+from\s+)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]?/m';
+ // https://regex101.com/r/fquriB/1
+ 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,
@@ -145,12 +145,11 @@ private function findAssetForBareImport(string $importedModule, AssetMapperInter
return null;
}
- // remote entries have no MappedAsset
- if ($importMapEntry->isRemotePackage()) {
- return null;
+ if ($asset = $assetMapper->getAsset($importMapEntry->path)) {
+ return $asset;
}
- return $assetMapper->getAsset($importMapEntry->path);
+ return $assetMapper->getAssetFromSourcePath($importMapEntry->path);
}
private function findAssetForRelativeImport(string $importedModule, MappedAsset $asset, AssetMapperInterface $assetMapper): ?MappedAsset
diff --git a/src/Symfony/Component/AssetMapper/Exception/LogicException.php b/src/Symfony/Component/AssetMapper/Exception/LogicException.php
new file mode 100644
index 0000000000000..c4cce726f3d2b
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/Exception/LogicException.php
@@ -0,0 +1,16 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\AssetMapper\Exception;
+
+class LogicException extends \LogicException implements ExceptionInterface
+{
+}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php
index 1d49e0c77055b..1597884b215bf 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php
@@ -35,10 +35,6 @@ public function audit(): array
{
$entries = $this->configReader->getEntries();
- if (!$entries) {
- return [];
- }
-
/** @var array> $installed */
$packageAudits = [];
@@ -51,14 +47,19 @@ public function audit(): array
}
$version = $entry->version;
- $installed[$entry->importName] ??= [];
- $installed[$entry->importName][] = $version;
+ $packageName = $entry->getPackageName();
+ $installed[$packageName] ??= [];
+ $installed[$packageName][] = $version;
- $packageVersion = $entry->importName.($version ? '@'.$version : '');
- $packageAudits[$packageVersion] ??= new ImportMapPackageAudit($entry->importName, $version);
+ $packageVersion = $packageName.'@'.$version;
+ $packageAudits[$packageVersion] ??= new ImportMapPackageAudit($packageName, $version);
$affectsQuery[] = $packageVersion;
}
+ if (!$affectsQuery) {
+ return [];
+ }
+
// @see https://docs.github.com/en/rest/security-advisories/global-advisories?apiVersion=2022-11-28#list-global-security-advisories
$response = $this->httpClient->request('GET', self::AUDIT_URL, [
'query' => ['affects' => implode(',', $affectsQuery)],
@@ -81,7 +82,7 @@ public function audit(): array
if (!$version || !$this->versionMatches($version, $vulnerability['vulnerable_version_range'] ?? '>= *')) {
continue;
}
- $packageAudits[$package.($version ? '@'.$version : '')] = $packageAudits[$package.($version ? '@'.$version : '')]->withVulnerability(
+ $packageAudits[$package.'@'.$version] = $packageAudits[$package.'@'.$version]->withVulnerability(
new ImportMapPackageAuditVulnerability(
$advisory['ghsa_id'],
$advisory['cve_id'],
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php
index 5b2a8240f7f4b..8aaee7a3e1646 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php
@@ -23,8 +23,10 @@ class ImportMapConfigReader
{
private ImportMapEntries $rootImportMapEntries;
- public function __construct(private readonly string $importMapConfigPath)
- {
+ public function __construct(
+ private readonly string $importMapConfigPath,
+ private readonly RemotePackageStorage $remotePackageStorage,
+ ) {
}
public function getEntries(): ImportMapEntries
@@ -38,7 +40,7 @@ public function getEntries(): ImportMapEntries
$entries = new ImportMapEntries();
foreach ($importMapConfig ?? [] as $importName => $data) {
- $validKeys = ['path', 'version', 'type', 'entrypoint', 'url'];
+ $validKeys = ['path', 'version', 'type', 'entrypoint', 'url', 'package_specifier'];
if ($invalidKeys = array_diff(array_keys($data), $validKeys)) {
throw new \InvalidArgumentException(sprintf('The following keys are not valid for the importmap entry "%s": "%s". Valid keys are: "%s".', $importName, implode('", "', $invalidKeys), implode('", "', $validKeys)));
}
@@ -49,36 +51,33 @@ public function getEntries(): ImportMapEntries
}
$type = isset($data['type']) ? ImportMapType::tryFrom($data['type']) : ImportMapType::JS;
- $isEntry = $data['entrypoint'] ?? false;
+ $isEntrypoint = $data['entrypoint'] ?? false;
+
+ if (isset($data['path'])) {
+ if (isset($data['version'])) {
+ throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
+ }
+ if (isset($data['package_specifier'])) {
+ throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "package_specifier" option.', $importName));
+ }
+
+ $entries->add(ImportMapEntry::createLocal($importName, $type, $data['path'], $isEntrypoint));
- if ($isEntry && ImportMapType::JS !== $type) {
- throw new RuntimeException(sprintf('The "entrypoint" option can only be used with the "js" type. Found "%s" in importmap.php for key "%s".', $importName, $type->value));
+ continue;
}
- $path = $data['path'] ?? null;
$version = $data['version'] ?? null;
if (null === $version && ($data['url'] ?? null)) {
// BC layer for 6.3->6.4
$version = $this->extractVersionFromLegacyUrl($data['url']);
}
- if (null === $version && null === $path) {
+
+ if (null === $version) {
throw new RuntimeException(sprintf('The importmap entry "%s" must have either a "path" or "version" option.', $importName));
}
- if (null !== $version && null !== $path) {
- throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
- }
- [$packageName, $filePath] = self::splitPackageNameAndFilePath($importName);
-
- $entries->add(new ImportMapEntry(
- $importName,
- path: $path,
- version: $version,
- type: $type,
- isEntrypoint: $isEntry,
- packageName: $packageName,
- filePath: $filePath,
- ));
+ $packageModuleSpecifier = $data['package_specifier'] ?? $importName;
+ $entries->add($this->createRemoteEntry($importName, $type, $version, $packageModuleSpecifier, $isEntrypoint));
}
return $this->rootImportMapEntries = $entries;
@@ -91,12 +90,13 @@ public function writeEntries(ImportMapEntries $entries): void
$importMapConfig = [];
foreach ($entries as $entry) {
$config = [];
- if ($entry->path) {
- $path = $entry->path;
- $config['path'] = $path;
- }
- if ($entry->version) {
+ if ($entry->isRemotePackage()) {
$config['version'] = $entry->version;
+ if ($entry->packageModuleSpecifier !== $entry->importName) {
+ $config['package_specifier'] = $entry->packageModuleSpecifier;
+ }
+ } else {
+ $config['path'] = $entry->path;
}
if (ImportMapType::JS !== $entry->type) {
$config['type'] = $entry->type->value;
@@ -104,6 +104,7 @@ public function writeEntries(ImportMapEntries $entries): void
if ($entry->isEntrypoint) {
$config['entrypoint'] = true;
}
+
$importMapConfig[$entry->importName] = $config;
}
@@ -129,6 +130,13 @@ public function writeEntries(ImportMapEntries $entries): void
EOF);
}
+ public function createRemoteEntry(string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint): ImportMapEntry
+ {
+ $path = $this->remotePackageStorage->getDownloadPath($packageModuleSpecifier, $type);
+
+ return ImportMapEntry::createRemote($importName, $type, $path, $version, $packageModuleSpecifier, $isEntrypoint);
+ }
+
public function getRootDirectory(): string
{
return \dirname($this->importMapConfigPath);
@@ -148,18 +156,4 @@ private function extractVersionFromLegacyUrl(string $url): ?string
return substr($url, $lastAt + 1, $nextSlash - $lastAt - 1);
}
-
- public static function splitPackageNameAndFilePath(string $packageName): array
- {
- $filePath = '';
- $i = strpos($packageName, '/');
-
- if ($i && (!str_starts_with($packageName, '@') || $i = strpos($packageName, '/', $i + 1))) {
- // @vendor/package/filepath or package/filepath
- $filePath = substr($packageName, $i);
- $packageName = substr($packageName, 0, $i);
- }
-
- return [$packageName, $filePath];
- }
}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php
index a2a92e9ed21e0..086dd2152c03b 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php
@@ -18,22 +18,65 @@
*/
final class ImportMapEntry
{
- public function __construct(
+ private function __construct(
public readonly string $importName,
+ public readonly ImportMapType $type,
/**
- * The path to the asset if local or downloaded.
+ * A logical path, relative path or absolute path to the file.
*/
- public readonly ?string $path = null,
- public readonly ?string $version = null,
- public readonly ImportMapType $type = ImportMapType::JS,
- public readonly bool $isEntrypoint = false,
- public readonly ?string $packageName = null,
- public readonly ?string $filePath = null,
+ public readonly string $path,
+ public readonly bool $isEntrypoint,
+ /**
+ * The version of the package (remote only).
+ */
+ public readonly ?string $version,
+ /**
+ * The full "package-name/path" (remote only).
+ */
+ public readonly ?string $packageModuleSpecifier,
) {
}
+ public static function createLocal(string $importName, ImportMapType $importMapType, string $path, bool $isEntrypoint): self
+ {
+ return new self($importName, $importMapType, $path, $isEntrypoint, null, null);
+ }
+
+ public static function createRemote(string $importName, ImportMapType $importMapType, string $path, string $version, string $packageModuleSpecifier, bool $isEntrypoint): self
+ {
+ return new self($importName, $importMapType, $path, $isEntrypoint, $version, $packageModuleSpecifier);
+ }
+
+ public function getPackageName(): string
+ {
+ return self::splitPackageNameAndFilePath($this->packageModuleSpecifier)[0];
+ }
+
+ public function getPackagePathString(): string
+ {
+ return self::splitPackageNameAndFilePath($this->packageModuleSpecifier)[1];
+ }
+
+ /**
+ * @psalm-assert-if-true !null $this->version
+ * @psalm-assert-if-true !null $this->packageModuleSpecifier
+ */
public function isRemotePackage(): bool
{
return null !== $this->version;
}
+
+ public static function splitPackageNameAndFilePath(string $packageModuleSpecifier): array
+ {
+ $filePath = '';
+ $i = strpos($packageModuleSpecifier, '/');
+
+ if ($i && (!str_starts_with($packageModuleSpecifier, '@') || $i = strpos($packageModuleSpecifier, '/', $i + 1))) {
+ // @vendor/package/filepath or package/filepath
+ $filePath = substr($packageModuleSpecifier, $i);
+ $packageModuleSpecifier = substr($packageModuleSpecifier, 0, $i);
+ }
+
+ return [$packageModuleSpecifier, $filePath];
+ }
}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
index 6144eab323f37..da16fffb769d0 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php
@@ -12,6 +12,7 @@
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;
@@ -154,19 +155,9 @@ public function getRawImportMapData(): array
$rawImportMapData = [];
foreach ($allEntries as $entry) {
- if ($entry->path) {
- $asset = $this->findAsset($entry->path);
-
- if (!$asset) {
- throw new \InvalidArgumentException(sprintf('The asset "%s" cannot be found in any asset map paths.', $entry->path));
- }
- } else {
- $sourcePath = $this->packageDownloader->getDownloadedPath($entry->importName);
- $asset = $this->assetMapper->getAssetFromSourcePath($sourcePath);
-
- if (!$asset) {
- throw new \InvalidArgumentException(sprintf('The "%s" vendor asset is missing. Run "php bin/console importmap:install".', $entry->importName));
- }
+ $asset = $this->findAsset($entry->path);
+ if (!$asset) {
+ throw $this->createMissingImportMapAssetException($entry);
}
$path = $asset->publicPath;
@@ -222,12 +213,8 @@ private function updateImportMapConfig(bool $update, array $packagesToRequire, a
continue;
}
- // assume the import name === package name, unless we can parse
- // the true package name from the URL
- $packageName = $importName;
-
$packagesToRequire[] = new PackageRequireOptions(
- $packageName,
+ $entry->packageModuleSpecifier,
null,
$importName,
);
@@ -267,7 +254,7 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp
$path = $requireOptions->path;
if (!$asset = $this->findAsset($path)) {
- throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found: either pass the logical name of the asset or a relative path starting with "./".', $requireOptions->path, $requireOptions->packageName));
+ throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found: either pass the logical name of the asset or a relative path starting with "./".', $requireOptions->path, $requireOptions->importName));
}
$rootImportMapDir = $this->importMapConfigReader->getRootDirectory();
@@ -277,11 +264,11 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp
$path = './'.substr(realpath($asset->sourcePath), \strlen(realpath($rootImportMapDir)) + 1);
}
- $newEntry = new ImportMapEntry(
- $requireOptions->packageName,
- path: $path,
- type: self::getImportMapTypeFromFilename($requireOptions->path),
- isEntrypoint: $requireOptions->entrypoint,
+ $newEntry = ImportMapEntry::createLocal(
+ $requireOptions->importName,
+ self::getImportMapTypeFromFilename($requireOptions->path),
+ $path,
+ $requireOptions->entrypoint,
);
$importMapEntries->add($newEntry);
$addedEntries[] = $newEntry;
@@ -294,14 +281,12 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp
$resolvedPackages = $this->resolver->resolvePackages($packagesToRequire);
foreach ($resolvedPackages as $resolvedPackage) {
- $importName = $resolvedPackage->requireOptions->importName ?: $resolvedPackage->requireOptions->packageName;
-
- $newEntry = new ImportMapEntry(
- $importName,
- path: $resolvedPackage->requireOptions->path,
- version: $resolvedPackage->version,
- type: $resolvedPackage->type,
- isEntrypoint: $resolvedPackage->requireOptions->entrypoint,
+ $newEntry = $this->importMapConfigReader->createRemoteEntry(
+ $resolvedPackage->requireOptions->importName,
+ $resolvedPackage->type,
+ $resolvedPackage->version,
+ $resolvedPackage->requireOptions->packageModuleSpecifier,
+ $resolvedPackage->requireOptions->entrypoint,
);
$importMapEntries->add($newEntry);
$addedEntries[] = $newEntry;
@@ -312,17 +297,9 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp
private function cleanupPackageFiles(ImportMapEntry $entry): void
{
- if (null === $entry->path) {
- return;
- }
-
$asset = $this->findAsset($entry->path);
- if (!$asset) {
- throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found in any asset map paths.', $entry->path, $entry->importName));
- }
-
- if (is_file($asset->sourcePath)) {
+ if ($asset && is_file($asset->sourcePath)) {
@unlink($asset->sourcePath);
}
}
@@ -345,14 +322,9 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE
return $currentImportEntries;
}
- // remote packages aren't in the asset mapper & so don't have dependencies
- if ($entry->isRemotePackage()) {
- return $currentImportEntries;
- }
-
if (!$asset = $this->findAsset($entry->path)) {
// should only be possible at this point for root importmap.php entries
- throw new \InvalidArgumentException(sprintf('The asset "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $entry->path));
+ throw $this->createMissingImportMapAssetException($entry);
}
foreach ($asset->getJavaScriptImports() as $javaScriptImport) {
@@ -363,14 +335,15 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE
continue;
}
- // check if this import requires an automatic importmap name
+ // check if this import requires an automatic importmap entry
if ($javaScriptImport->addImplicitlyToImportMap && $javaScriptImport->asset) {
- $nextEntry = new ImportMapEntry(
+ $nextEntry = ImportMapEntry::createLocal(
$importName,
- path: $javaScriptImport->asset->logicalPath,
- type: ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS,
- isEntrypoint: false,
+ ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS,
+ $javaScriptImport->asset->logicalPath,
+ false,
);
+
$currentImportEntries[$importName] = $nextEntry;
} else {
$nextEntry = $this->findRootImportMapEntry($importName);
@@ -457,4 +430,13 @@ 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/ImportMapUpdateChecker.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapUpdateChecker.php
index 0a77f8e7ba038..b64a067609850 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapUpdateChecker.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapUpdateChecker.php
@@ -34,27 +34,30 @@ public function getAvailableUpdates(array $packages = []): array
$updateInfos = [];
$responses = [];
foreach ($entries as $entry) {
- if (null === $entry->packageName || null === $entry->version) {
+ if (!$entry->isRemotePackage()) {
continue;
}
- if (\count($packages) && !\in_array($entry->packageName, $packages, true)) {
+ if ($packages
+ && !\in_array($entry->getPackageName(), $packages, true)
+ && !\in_array($entry->importName, $packages, true)
+ ) {
continue;
}
- $responses[$entry->importName] = $this->httpClient->request('GET', sprintf(self::URL_PACKAGE_METADATA, $entry->packageName), ['headers' => ['Accept' => 'application/vnd.npm.install-v1+json']]);
+ $responses[$entry->importName] = $this->httpClient->request('GET', sprintf(self::URL_PACKAGE_METADATA, $entry->getPackageName()), ['headers' => ['Accept' => 'application/vnd.npm.install-v1+json']]);
}
foreach ($responses as $importName => $response) {
$entry = $entries->get($importName);
if (200 !== $response->getStatusCode()) {
- throw new \RuntimeException(sprintf('Unable to get latest version for package "%s".', $entry->packageName));
+ throw new \RuntimeException(sprintf('Unable to get latest version for package "%s".', $entry->getPackageName()));
}
- $updateInfo = new PackageUpdateInfo($entry->packageName, $entry->version);
+ $updateInfo = new PackageUpdateInfo($entry->getPackageName(), $entry->version);
try {
$updateInfo->latestVersion = json_decode($response->getContent(), true)['dist-tags']['latest'];
$updateInfo->updateType = $this->getUpdateType($updateInfo->currentVersion, $updateInfo->latestVersion);
} catch (\Exception $e) {
- throw new \RuntimeException(sprintf('Unable to get latest version for package "%s".', $entry->packageName), 0, $e);
+ throw new \RuntimeException(sprintf('Unable to get latest version for package "%s".', $entry->getPackageName()), 0, $e);
}
$updateInfos[$importName] = $updateInfo;
}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/JavaScriptImport.php b/src/Symfony/Component/AssetMapper/ImportMap/JavaScriptImport.php
index d7d070e587bd1..12030934ff3b9 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/JavaScriptImport.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/JavaScriptImport.php
@@ -19,9 +19,10 @@
final class JavaScriptImport
{
/**
- * @param string $importName The name of the import needed in the importmap, e.g. "/foo.js" or "react".
- * @param bool $isLazy whether this import was lazy or eager
- * @param bool $addImplicitlyToImportMap whether this import should be added to the importmap automatically
+ * @param string $importName The name of the import needed in the importmap, e.g. "/foo.js" or "react"
+ * @param bool $isLazy Whether this import was lazy or eager
+ * @param MappedAsset|null $asset The asset that was imported, if known - needed to add to the importmap, also used to find further imports for preloading
+ * @param bool $addImplicitlyToImportMap Whether this import should be added to the importmap automatically
*/
public function __construct(
public readonly string $importName,
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php b/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php
index 095533c69f07c..6875bca9d1e59 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php
@@ -18,12 +18,18 @@
*/
final class PackageRequireOptions
{
+ public readonly string $importName;
+
public function __construct(
- public readonly string $packageName,
+ /**
+ * The "package-name/path" of the remote package.
+ */
+ public readonly string $packageModuleSpecifier,
public readonly ?string $versionConstraint = null,
- public readonly ?string $importName = null,
+ string $importName = null,
public readonly ?string $path = null,
public readonly bool $entrypoint = false,
) {
+ $this->importName = $importName ?: $packageModuleSpecifier;
}
}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php b/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php
index a3440473ab792..577abfd5e7236 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php
@@ -21,9 +21,9 @@ class RemotePackageDownloader
private array $installed;
public function __construct(
+ private readonly RemotePackageStorage $remotePackageStorage,
private readonly ImportMapConfigReader $importMapConfigReader,
private readonly PackageResolverInterface $packageResolver,
- private readonly string $vendorDir,
) {
}
@@ -51,7 +51,7 @@ public function downloadPackages(callable $progressCallback = null): array
if (
isset($installed[$entry->importName])
&& $installed[$entry->importName]['version'] === $entry->version
- && file_exists($this->vendorDir.'/'.$installed[$entry->importName]['path'])
+ && $this->remotePackageStorage->isDownloaded($entry)
) {
$newInstalled[$entry->importName] = $installed[$entry->importName];
continue;
@@ -71,9 +71,8 @@ public function downloadPackages(callable $progressCallback = null): array
throw new \LogicException(sprintf('The package "%s" was not downloaded.', $package));
}
- $filename = $this->savePackage($package, $contents[$package], $entry->type);
+ $this->remotePackageStorage->save($entry, $contents[$package]);
$newInstalled[$package] = [
- 'path' => $filename,
'version' => $entry->version,
];
@@ -90,33 +89,9 @@ public function downloadPackages(callable $progressCallback = null): array
return $downloadedPackages;
}
- public function getDownloadedPath(string $importName): string
- {
- $installed = $this->loadInstalled();
- if (!isset($installed[$importName])) {
- throw new \InvalidArgumentException(sprintf('The "%s" vendor asset is missing. Run "php bin/console importmap:install".', $importName));
- }
-
- return $this->vendorDir.'/'.$installed[$importName]['path'];
- }
-
public function getVendorDir(): string
{
- return $this->vendorDir;
- }
-
- private function savePackage(string $packageName, string $packageContents, ImportMapType $importMapType): string
- {
- $filename = $packageName;
- if (!str_contains(basename($packageName), '.')) {
- $filename .= '.'.$importMapType->value;
- }
- $vendorPath = $this->vendorDir.'/'.$filename;
-
- @mkdir(\dirname($vendorPath), 0777, true);
- file_put_contents($vendorPath, $packageContents);
-
- return $filename;
+ return $this->remotePackageStorage->getStorageDir();
}
/**
@@ -128,31 +103,21 @@ private function loadInstalled(): array
return $this->installed;
}
- $installedPath = $this->vendorDir.'/installed.php';
+ $installedPath = $this->remotePackageStorage->getStorageDir().'/installed.php';
$installed = is_file($installedPath) ? (static fn () => include $installedPath)() : [];
foreach ($installed as $package => $data) {
- if (!isset($data['path'])) {
- throw new \InvalidArgumentException(sprintf('The package "%s" is missing its path.', $package));
- }
-
if (!isset($data['version'])) {
throw new \InvalidArgumentException(sprintf('The package "%s" is missing its version.', $package));
}
-
- if (!is_file($this->vendorDir.'/'.$data['path'])) {
- unset($installed[$package]);
- }
}
- $this->installed = $installed;
-
- return $installed;
+ return $this->installed = $installed;
}
private function saveInstalled(array $installed): void
{
$this->installed = $installed;
- file_put_contents($this->vendorDir.'/installed.php', sprintf('remotePackageStorage->getStorageDir().'/installed.php', sprintf('
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\AssetMapper\ImportMap;
+
+/**
+ * Manages the local storage of remote/vendor importmap packages.
+ */
+class RemotePackageStorage
+{
+ public function __construct(private readonly string $vendorDir)
+ {
+ }
+
+ public function getStorageDir(): string
+ {
+ return $this->vendorDir;
+ }
+
+ public function isDownloaded(ImportMapEntry $entry): bool
+ {
+ if (!$entry->isRemotePackage()) {
+ throw new \InvalidArgumentException(sprintf('The entry "%s" is not a remote package.', $entry->importName));
+ }
+
+ return is_file($this->getDownloadPath($entry->packageModuleSpecifier, $entry->type));
+ }
+
+ public function save(ImportMapEntry $entry, string $contents): void
+ {
+ if (!$entry->isRemotePackage()) {
+ throw new \InvalidArgumentException(sprintf('The entry "%s" is not a remote package.', $entry->importName));
+ }
+
+ $vendorPath = $this->getDownloadPath($entry->packageModuleSpecifier, $entry->type);
+
+ @mkdir(\dirname($vendorPath), 0777, true);
+ file_put_contents($vendorPath, $contents);
+ }
+
+ /**
+ * The local file path where a downloaded package should be stored.
+ */
+ public function getDownloadPath(string $packageModuleSpecifier, ImportMapType $importMapType): string
+ {
+ [$packageName, $packagePathString] = ImportMapEntry::splitPackageNameAndFilePath($packageModuleSpecifier);
+ $filename = $packageName;
+ if ($packagePathString) {
+ $filename .= '/'.ltrim($packagePathString, '/');
+ } else {
+ // if we're requiring a bare package, we put it into the directory
+ // (in case we also import other files from the package) and arbitrarily
+ // name it the same as the package name + ".index"
+ $filename .= '/'.basename($packageName).'.index';
+ }
+
+ if (!str_ends_with($filename, '.'.$importMapType->value)) {
+ $filename .= '.'.$importMapType->value;
+ }
+
+ return $this->vendorDir.'/'.$filename;
+ }
+}
diff --git a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php
index a14a8f0ac5e7b..9b9f395a3a0aa 100644
--- a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php
+++ b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JsDelivrEsmResolver.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\AssetMapper\ImportMap\Resolver;
use Symfony\Component\AssetMapper\Exception\RuntimeException;
-use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions;
@@ -49,26 +48,26 @@ public function resolvePackages(array $packagesToRequire): array
// request the version of each package
$requiredPackages = [];
foreach ($packagesToRequire as $options) {
- $packageName = trim($options->packageName, '/');
+ $packageSpecifier = trim($options->packageModuleSpecifier, '/');
$constraint = $options->versionConstraint ?? '*';
// avoid resolving the same package twice
- if (isset($resolvedPackages[$packageName])) {
+ if (isset($resolvedPackages[$packageSpecifier])) {
continue;
}
- [$packageName, $filePath] = ImportMapConfigReader::splitPackageNameAndFilePath($packageName);
+ [$packageName, $filePath] = ImportMapEntry::splitPackageNameAndFilePath($packageSpecifier);
$response = $this->httpClient->request('GET', sprintf($this->versionUrlPattern, $packageName, urlencode($constraint)));
$requiredPackages[] = [$options, $response, $packageName, $filePath, /* resolved version */ null];
}
- // grab the version of each package & request the contents
- $errors = [];
- $cssEntrypointResponses = [];
+ // use the version of each package to request the contents
+ $findVersionErrors = [];
+ $entrypointResponses = [];
foreach ($requiredPackages as $i => [$options, $response, $packageName, $filePath]) {
if (200 !== $response->getStatusCode()) {
- $errors[] = [$options->packageName, $response];
+ $findVersionErrors[] = [$packageName, $response];
continue;
}
@@ -78,49 +77,49 @@ public function resolvePackages(array $packagesToRequire): array
$requiredPackages[$i][4] = $version;
if (!$filePath) {
- $cssEntrypointResponses[$packageName] = $this->httpClient->request('GET', sprintf(self::URL_PATTERN_ENTRYPOINT, $packageName, $version));
+ $entrypointResponses[$packageName] = [$this->httpClient->request('GET', sprintf(self::URL_PATTERN_ENTRYPOINT, $packageName, $version)), $version];
}
}
try {
- ($errors[0][1] ?? null)?->getHeaders();
+ ($findVersionErrors[0][1] ?? null)?->getHeaders();
} catch (HttpExceptionInterface $e) {
$response = $e->getResponse();
- $packages = implode('", "', array_column($errors, 0));
+ $packages = implode('", "', array_column($findVersionErrors, 0));
- throw new RuntimeException(sprintf('Error %d finding version from jsDelivr for "%s". Check your package names. Response: ', $response->getStatusCode(), $packages).$response->getContent(false), 0, $e);
+ throw new RuntimeException(sprintf('Error %d finding version from jsDelivr for the following packages: "%s". Check your package names. Response: ', $response->getStatusCode(), $packages).$response->getContent(false), 0, $e);
}
// process the contents of each package & add the resolved package
$packagesToRequire = [];
+ $getContentErrors = [];
foreach ($requiredPackages as [$options, $response, $packageName, $filePath, $version]) {
if (200 !== $response->getStatusCode()) {
- $errors[] = [$options->packageName, $response];
+ $getContentErrors[] = [$options->packageModuleSpecifier, $response];
continue;
}
- $packageName = trim($options->packageName, '/');
$contentType = $response->getHeaders()['content-type'][0] ?? '';
$type = str_starts_with($contentType, 'text/css') ? ImportMapType::CSS : ImportMapType::JS;
- $resolvedPackages[$packageName] = new ResolvedImportMapPackage($options, $version, $type);
+ $resolvedPackages[$options->packageModuleSpecifier] = new ResolvedImportMapPackage($options, $version, $type);
$packagesToRequire = array_merge($packagesToRequire, $this->fetchPackageRequirementsFromImports($response->getContent()));
}
try {
- ($errors[0][1] ?? null)?->getHeaders();
+ ($getContentErrors[0][1] ?? null)?->getHeaders();
} catch (HttpExceptionInterface $e) {
$response = $e->getResponse();
- $packages = implode('", "', array_column($errors, 0));
+ $packages = implode('", "', array_column($getContentErrors, 0));
throw new RuntimeException(sprintf('Error %d requiring packages from jsDelivr for "%s". Check your package names. Response: ', $response->getStatusCode(), $packages).$response->getContent(false), 0, $e);
}
// process any pending CSS entrypoints
- $errors = [];
- foreach ($cssEntrypointResponses as $package => $cssEntrypointResponse) {
+ $entrypointErrors = [];
+ foreach ($entrypointResponses as $package => [$cssEntrypointResponse, $version]) {
if (200 !== $cssEntrypointResponse->getStatusCode()) {
- $errors[] = [$package, $cssEntrypointResponse];
+ $entrypointErrors[] = [$package, $cssEntrypointResponse];
continue;
}
@@ -135,12 +134,12 @@ public function resolvePackages(array $packagesToRequire): array
}
try {
- ($errors[0][1] ?? null)?->getHeaders();
+ ($entrypointErrors[0][1] ?? null)?->getHeaders();
} catch (HttpExceptionInterface $e) {
$response = $e->getResponse();
- $packages = implode('", "', array_column($errors, 0));
+ $packages = implode('", "', array_column($entrypointErrors, 0));
- throw new RuntimeException(sprintf('Error %d checking for a CSS entrypoint for packages from jsDelivr for "%s". Response: ', $response->getStatusCode(), $packages).$response->getContent(false), 0, $e);
+ throw new RuntimeException(sprintf('Error %d checking for a CSS entrypoint for "%s". Response: ', $response->getStatusCode(), $packages).$response->getContent(false), 0, $e);
}
if ($packagesToRequire) {
@@ -160,8 +159,12 @@ public function downloadPackages(array $importMapEntries, callable $progressCall
$responses = [];
foreach ($importMapEntries as $package => $entry) {
+ if (!$entry->isRemotePackage()) {
+ throw new \InvalidArgumentException(sprintf('The entry "%s" is not a remote package.', $entry->importName));
+ }
+
$pattern = ImportMapType::CSS === $entry->type ? $this->distUrlCssPattern : $this->distUrlPattern;
- $url = sprintf($pattern, $entry->packageName, $entry->version, $entry->filePath);
+ $url = sprintf($pattern, $entry->getPackageName(), $entry->version, $entry->getPackagePathString());
$responses[$package] = $this->httpClient->request('GET', $url);
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php
index 74642c012ee3e..544d0c543a034 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php
@@ -78,10 +78,10 @@ public function testAssetsAreCompiled()
'file2.js',
'file3.css',
'file4.js',
- 'lodash.js',
- 'stimulus.js',
'subdir/file5.js',
'subdir/file6.js',
+ 'vendor/@hotwired/stimulus/stimulus.index.js',
+ 'vendor/lodash/lodash.index.js',
], array_keys(json_decode(file_get_contents($targetBuildDir.'/manifest.json'), true)));
$this->assertFileExists($targetBuildDir.'/importmap.json');
diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php
index 8f375876078ff..3d91d2f7d2d13 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Command/DebugAssetsMapperCommandTest.php
@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
-namespace Command;
+namespace Symfony\Component\AssetMapper\Tests\Command;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
diff --git a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
index e30c4361e93dc..bf4c79e25bc1c 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php
@@ -20,6 +20,7 @@
use Symfony\Component\AssetMapper\Exception\RuntimeException;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapManager;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\AssetMapper\MappedAsset;
class JavaScriptImportPathCompilerTest extends TestCase
@@ -35,15 +36,12 @@ public function testCompile(string $sourceLogicalName, string $input, array $exp
$importMapManager->expects($this->any())
->method('findRootImportMapEntry')
->willReturnCallback(function ($importName) {
- if ('module_in_importmap_local_asset' === $importName) {
- return new ImportMapEntry('module_in_importmap_local_asset', 'module_in_importmap_local_asset.js');
- }
-
- if ('module_in_importmap_remote' === $importName) {
- return new ImportMapEntry('module_in_importmap_local_asset', version: '1.2.3');
- }
-
- return null;
+ return match ($importName) {
+ 'module_in_importmap_local_asset' => ImportMapEntry::createLocal('module_in_importmap_local_asset', ImportMapType::JS, 'module_in_importmap_local_asset.js', false),
+ 'module_in_importmap_remote' => ImportMapEntry::createRemote('module_in_importmap_remote', ImportMapType::JS, '/path/to/vendor/module_in_importmap_remote.js', '1.2.3', 'could_be_anything', false),
+ '@popperjs/core' => ImportMapEntry::createRemote('@popperjs/core', ImportMapType::JS, '/path/to/vendor/@popperjs/core.js', '1.2.3', 'could_be_anything', false),
+ default => null,
+ };
});
$compiler = new JavaScriptImportPathCompiler($importMapManager);
// compile - and check that content doesn't change
@@ -284,7 +282,7 @@ public static function provideCompileTests(): iterable
yield 'bare_import_in_importmap_but_remote' => [
'sourceLogicalName' => 'app.js',
'input' => 'import "module_in_importmap_remote";',
- 'expectedJavaScriptImports' => ['module_in_importmap_remote' => ['lazy' => false, 'asset' => null, 'add' => false]],
+ 'expectedJavaScriptImports' => ['module_in_importmap_remote' => ['lazy' => false, 'asset' => 'module_in_importmap_remote.js', 'add' => false]],
];
yield 'absolute_import_added_as_dependency_only' => [
@@ -292,6 +290,12 @@ public static function provideCompileTests(): iterable
'input' => 'import "https://example.com/module.js";',
'expectedJavaScriptImports' => ['https://example.com/module.js' => ['lazy' => false, 'asset' => null, 'add' => false]],
];
+
+ yield 'bare_import_with_minimal_spaces' => [
+ 'sourceLogicalName' => 'app.js',
+ 'input' => 'import*as t from"@popperjs/core";',
+ 'expectedJavaScriptImports' => ['@popperjs/core' => ['lazy' => false, 'asset' => 'assets/vendor/@popperjs/core.js', 'add' => false]],
+ ];
}
/**
@@ -450,6 +454,16 @@ private function createAssetMapper(): AssetMapperInterface
};
});
+ $assetMapper->expects($this->any())
+ ->method('getAssetFromSourcePath')
+ ->willReturnCallback(function ($path) {
+ return match ($path) {
+ '/path/to/vendor/module_in_importmap_remote.js' => new MappedAsset('module_in_importmap_remote.js', publicPathWithoutDigest: '/assets/module_in_importmap_remote.js'),
+ '/path/to/vendor/@popperjs/core.js' => new MappedAsset('assets/vendor/@popperjs/core.js', publicPathWithoutDigest: '/assets/@popperjs/core.js'),
+ default => null,
+ };
+ });
+
return $assetMapper;
}
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/Factory/CachedMappedAssetFactoryTest.php b/src/Symfony/Component/AssetMapper/Tests/Factory/CachedMappedAssetFactoryTest.php
index 36b8a0578a76c..d334e3863954d 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Factory/CachedMappedAssetFactoryTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Factory/CachedMappedAssetFactoryTest.php
@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
-namespace Factory;
+namespace Symfony\Component\AssetMapper\Tests\Factory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\AssetMapper\Factory\CachedMappedAssetFactory;
diff --git a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
index c4b09bec5056a..d4131ae39c377 100644
--- a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
@@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
-namespace Factory;
+namespace Symfony\Component\AssetMapper\Tests\Factory;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
@@ -116,7 +116,7 @@ public function testCreateMappedAssetWithPredigested()
public function testCreateMappedAssetInVendor()
{
$assetMapper = $this->createFactory();
- $asset = $assetMapper->createMappedAsset('lodash.js', __DIR__.'/../fixtures/assets/vendor/lodash.js');
+ $asset = $assetMapper->createMappedAsset('lodash.js', __DIR__.'/../fixtures/assets/vendor/lodash/lodash.index.js');
$this->assertSame('lodash.js', $asset->logicalPath);
$this->assertTrue($asset->isVendor);
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php
index 07e6512696dea..952f9987b3012 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php
@@ -19,6 +19,7 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapPackageAudit;
use Symfony\Component\AssetMapper\ImportMap\ImportMapPackageAuditVulnerability;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -65,18 +66,9 @@ public function testAudit()
],
])));
$this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([
- '@hotwired/stimulus' => new ImportMapEntry(
- importName: '@hotwired/stimulus',
- version: '3.2.1',
- ),
- 'json5' => new ImportMapEntry(
- importName: 'json5',
- version: '1.0.0',
- ),
- 'lodash' => new ImportMapEntry(
- importName: 'lodash',
- version: '4.17.21',
- ),
+ self::createRemoteEntry('@hotwired/stimulus', '3.2.1'),
+ self::createRemoteEntry('json5/some/file', '1.0.0'),
+ self::createRemoteEntry('lodash', '4.17.21'),
]));
$audit = $this->importMapAuditor->audit();
@@ -118,10 +110,7 @@ public function testAuditWithVersionRange(bool $expectMatch, string $version, ?s
],
])));
$this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([
- 'json5' => new ImportMapEntry(
- importName: 'json5',
- version: $version,
- ),
+ self::createRemoteEntry('json5', $version),
]));
$audit = $this->importMapAuditor->audit();
@@ -146,10 +135,7 @@ public function testAuditError()
{
$this->httpClient->setResponseFactory(new MockResponse('Server error', ['http_code' => 500]));
$this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([
- 'json5' => new ImportMapEntry(
- importName: 'json5',
- version: '1.0.0',
- ),
+ self::createRemoteEntry('json5', '1.0.0'),
]));
$this->expectException(RuntimeException::class);
@@ -157,4 +143,16 @@ public function testAuditError()
$this->importMapAuditor->audit();
}
+
+ private static function createRemoteEntry(string $packageSpecifier, string $version): ImportMapEntry
+ {
+ return ImportMapEntry::createRemote(
+ 'could_by_anything'.md5($packageSpecifier.$version),
+ ImportMapType::JS,
+ '/any/path',
+ $version,
+ $packageSpecifier,
+ false
+ );
+ }
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php
index 1598f9b1acb30..5cfbf76c5e70b 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php
@@ -15,6 +15,8 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
+use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
use Symfony\Component\Filesystem\Filesystem;
class ImportMapConfigReaderTest extends TestCase
@@ -59,43 +61,43 @@ public function testGetEntriesAndWriteEntries()
'package/with_file.js' => [
'version' => '1.0.0',
],
- '@vendor/package/path/to/file.js' => [
- 'version' => '1.0.0',
- ],
];
EOF;
file_put_contents(__DIR__.'/../fixtures/importmaps_for_writing/importmap.php', $importMap);
- $reader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmaps_for_writing/importmap.php');
+ $remotePackageStorage = $this->createMock(RemotePackageStorage::class);
+ $remotePackageStorage->expects($this->any())
+ ->method('getDownloadPath')
+ ->willReturnCallback(static function (string $packageModuleSpecifier, ImportMapType $type) {
+ return '/path/to/vendor/'.$packageModuleSpecifier.'.'.$type->value;
+ });
+ $reader = new ImportMapConfigReader(
+ __DIR__.'/../fixtures/importmaps_for_writing/importmap.php',
+ $remotePackageStorage,
+ );
$entries = $reader->getEntries();
$this->assertInstanceOf(ImportMapEntries::class, $entries);
/** @var ImportMapEntry[] $allEntries */
$allEntries = iterator_to_array($entries);
- $this->assertCount(6, $allEntries);
+ $this->assertCount(5, $allEntries);
$remotePackageEntry = $allEntries[0];
$this->assertSame('remote_package', $remotePackageEntry->importName);
- $this->assertNull($remotePackageEntry->path);
+ $this->assertSame('/path/to/vendor/remote_package.js', $remotePackageEntry->path);
$this->assertSame('3.2.1', $remotePackageEntry->version);
$this->assertSame('js', $remotePackageEntry->type->value);
$this->assertFalse($remotePackageEntry->isEntrypoint);
- $this->assertSame('remote_package', $remotePackageEntry->packageName);
- $this->assertEquals('', $remotePackageEntry->filePath);
+ $this->assertSame('remote_package', $remotePackageEntry->packageModuleSpecifier);
$localPackageEntry = $allEntries[1];
- $this->assertNull($localPackageEntry->version);
+ $this->assertFalse($localPackageEntry->isRemotePackage());
$this->assertSame('app.js', $localPackageEntry->path);
$typeCssEntry = $allEntries[2];
$this->assertSame('css', $typeCssEntry->type->value);
$packageWithFileEntry = $allEntries[4];
- $this->assertSame('package', $packageWithFileEntry->packageName);
- $this->assertSame('/with_file.js', $packageWithFileEntry->filePath);
-
- $packageWithFileEntry = $allEntries[5];
- $this->assertSame('@vendor/package', $packageWithFileEntry->packageName);
- $this->assertSame('/path/to/file.js', $packageWithFileEntry->filePath);
+ $this->assertSame('package/with_file.js', $packageWithFileEntry->packageModuleSpecifier);
// now save the original raw data from importmap.php and delete the file
$originalImportMapData = (static fn () => include __DIR__.'/../fixtures/importmaps_for_writing/importmap.php')();
@@ -109,7 +111,7 @@ public function testGetEntriesAndWriteEntries()
public function testGetRootDirectory()
{
- $configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php');
+ $configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php', $this->createMock(RemotePackageStorage::class));
$this->assertSame(__DIR__.'/../fixtures', $configReader->getRootDirectory());
}
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntriesTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntriesTest.php
index c7cdfeda9e42d..6de9a6c2a4197 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntriesTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntriesTest.php
@@ -14,13 +14,14 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries;
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
class ImportMapEntriesTest extends TestCase
{
public function testGetIterator()
{
- $entry1 = new ImportMapEntry('entry1', 'path1');
- $entry2 = new ImportMapEntry('entry2', 'path2');
+ $entry1 = ImportMapEntry::createLocal('entry1', ImportMapType::JS, 'path1', true);
+ $entry2 = ImportMapEntry::createLocal('entry2', ImportMapType::CSS, 'path2', false);
$entries = new ImportMapEntries([$entry1]);
$entries->add($entry2);
@@ -30,7 +31,7 @@ public function testGetIterator()
public function testHas()
{
- $entries = new ImportMapEntries([new ImportMapEntry('entry1', 'path1')]);
+ $entries = new ImportMapEntries([ImportMapEntry::createLocal('entry1', ImportMapType::JS, 'path1', true)]);
$this->assertTrue($entries->has('entry1'));
$this->assertFalse($entries->has('entry2'));
@@ -38,7 +39,7 @@ public function testHas()
public function testGet()
{
- $entry = new ImportMapEntry('entry1', 'path1');
+ $entry = ImportMapEntry::createLocal('entry1', ImportMapType::JS, 'path1', false);
$entries = new ImportMapEntries([$entry]);
$this->assertSame($entry, $entries->get('entry1'));
@@ -46,7 +47,7 @@ public function testGet()
public function testRemove()
{
- $entries = new ImportMapEntries([new ImportMapEntry('entry1', 'path1')]);
+ $entries = new ImportMapEntries([ImportMapEntry::createLocal('entry1', ImportMapType::JS, 'path1', true)]);
$entries->remove('entry1');
$this->assertFalse($entries->has('entry1'));
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntryTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntryTest.php
new file mode 100644
index 0000000000000..808fd1adcad76
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapEntryTest.php
@@ -0,0 +1,85 @@
+
+ *
+ * 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\TestCase;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
+
+class ImportMapEntryTest extends TestCase
+{
+ public function testCreateLocal()
+ {
+ $entry = ImportMapEntry::createLocal('foo', ImportMapType::JS, 'foo.js', true);
+ $this->assertSame('foo', $entry->importName);
+ $this->assertSame(ImportMapType::JS, $entry->type);
+ $this->assertSame('foo.js', $entry->path);
+ $this->assertTrue($entry->isEntrypoint);
+ $this->assertFalse($entry->isRemotePackage());
+ }
+
+ public function testCreateRemote()
+ {
+ $entry = ImportMapEntry::createRemote('foo', ImportMapType::JS, 'foo.js', '1.0.0', 'foo/bar', true);
+ $this->assertSame('foo', $entry->importName);
+ $this->assertSame(ImportMapType::JS, $entry->type);
+ $this->assertSame('foo.js', $entry->path);
+ $this->assertTrue($entry->isEntrypoint);
+ $this->assertTrue($entry->isRemotePackage());
+ $this->assertSame('1.0.0', $entry->version);
+ $this->assertSame('foo/bar', $entry->packageModuleSpecifier);
+ }
+
+ /**
+ * @dataProvider getSplitPackageNameTests
+ */
+ public function testSplitPackageNameAndFilePath(string $packageModuleSpecifier, string $expectedPackage, string $expectedPath)
+ {
+ [$actualPackage, $actualPath] = ImportMapEntry::splitPackageNameAndFilePath($packageModuleSpecifier);
+ $this->assertSame($expectedPackage, $actualPackage);
+ $this->assertSame($expectedPath, $actualPath);
+ }
+
+ public static function getSplitPackageNameTests()
+ {
+ yield 'package-name' => [
+ 'package-name',
+ 'package-name',
+ '',
+ ];
+
+ yield 'package-name/path' => [
+ 'package-name/path',
+ 'package-name',
+ '/path',
+ ];
+
+ yield '@scope/package-name' => [
+ '@scope/package-name',
+ '@scope/package-name',
+ '',
+ ];
+
+ yield '@scope/package-name/path' => [
+ '@scope/package-name/path',
+ '@scope/package-name',
+ '/path',
+ ];
+ }
+
+ public function testGetPackageNameAndPackagePath()
+ {
+ $entry = ImportMapEntry::createRemote('foo', ImportMapType::JS, 'foo.js', '1.0.0', 'foo/bar', true);
+ $this->assertSame('foo', $entry->getPackageName());
+ $this->assertSame('/bar', $entry->getPackagePathString());
+ }
+}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
index 80b9cd47602ea..6c42c6df051e3 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php
@@ -64,7 +64,6 @@ public function testGetRawImportMapData(array $importMapEntries, array $mappedAs
$manager = $this->createImportMapManager();
$this->mockImportMap($importMapEntries);
$this->mockAssetMapper($mappedAssets);
- $this->mockDownloader($importMapEntries);
$this->configReader->expects($this->any())
->method('getRootDirectory')
->willReturn('/fake/root');
@@ -76,16 +75,16 @@ public function getRawImportMapDataTests(): iterable
{
yield 'it returns remote downloaded entry' => [
[
- new ImportMapEntry(
+ self::createRemoteEntry(
'@hotwired/stimulus',
version: '1.2.3',
- packageName: '@hotwired/stimulus',
+ path: '/assets/vendor/stimulus.js'
),
],
[
new MappedAsset(
'vendor/@hotwired/stimulus.js',
- self::$writableRoot.'/assets/vendor/@hotwired/stimulus.js',
+ '/assets/vendor/stimulus.js',
publicPath: '/assets/vendor/@hotwired/stimulus-d1g35t.js',
),
],
@@ -99,20 +98,20 @@ public function getRawImportMapDataTests(): iterable
yield 'it returns basic local javascript file' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
- path: 'app.js',
+ path: 'app.js'
),
],
[
new MappedAsset(
'app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d13g35t.js',
),
],
[
'app' => [
- 'path' => '/assets/app.js',
+ 'path' => '/assets/app-d13g35t.js',
'type' => 'js',
],
],
@@ -120,7 +119,7 @@ public function getRawImportMapDataTests(): iterable
yield 'it returns basic local css file' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app.css',
path: 'styles/app.css',
type: ImportMapType::CSS,
@@ -129,12 +128,12 @@ public function getRawImportMapDataTests(): iterable
[
new MappedAsset(
'styles/app.css',
- publicPath: '/assets/styles/app.css',
+ publicPath: '/assets/styles/app-d13g35t.css',
),
],
[
'app.css' => [
- 'path' => '/assets/styles/app.css',
+ 'path' => '/assets/styles/app-d13g35t.css',
'type' => 'css',
],
],
@@ -147,7 +146,7 @@ public function getRawImportMapDataTests(): iterable
);
yield 'it adds dependency to the importmap' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
path: 'app.js',
),
@@ -155,14 +154,43 @@ public function getRawImportMapDataTests(): iterable
[
new MappedAsset(
'app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
javaScriptImports: [new JavaScriptImport('/assets/simple.js', isLazy: false, asset: $simpleAsset, addImplicitlyToImportMap: true)]
),
$simpleAsset,
],
[
'app' => [
- 'path' => '/assets/app.js',
+ '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' => [
@@ -180,7 +208,7 @@ public function getRawImportMapDataTests(): iterable
);
yield 'it processes imports recursively' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
path: 'app.js',
),
@@ -188,7 +216,7 @@ public function getRawImportMapDataTests(): iterable
[
new MappedAsset(
'app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
javaScriptImports: [new JavaScriptImport('/assets/imports_simple.js', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: true)]
),
$eagerImportsSimpleAsset,
@@ -196,7 +224,7 @@ public function getRawImportMapDataTests(): iterable
],
[
'app' => [
- 'path' => '/assets/app.js',
+ 'path' => '/assets/app-d1g3st.js',
'type' => 'js',
],
'/assets/imports_simple.js' => [
@@ -212,11 +240,11 @@ public function getRawImportMapDataTests(): iterable
yield 'it process can skip adding one importmap entry but still add a child' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
path: 'app.js',
),
- new ImportMapEntry(
+ self::createLocalEntry(
'imports_simple',
path: 'imports_simple.js',
),
@@ -224,7 +252,7 @@ public function getRawImportMapDataTests(): iterable
[
new MappedAsset(
'app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
javaScriptImports: [new JavaScriptImport('imports_simple', isLazy: true, asset: $eagerImportsSimpleAsset, addImplicitlyToImportMap: false)]
),
$eagerImportsSimpleAsset,
@@ -232,7 +260,7 @@ public function getRawImportMapDataTests(): iterable
],
[
'app' => [
- 'path' => '/assets/app.js',
+ 'path' => '/assets/app-d1g3st.js',
'type' => 'js',
],
'/assets/simple.js' => [
@@ -248,7 +276,7 @@ public function getRawImportMapDataTests(): iterable
yield 'imports with a module name are not added to the importmap' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
path: 'app.js',
),
@@ -256,14 +284,14 @@ public function getRawImportMapDataTests(): iterable
[
new MappedAsset(
'app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
javaScriptImports: [new JavaScriptImport('simple', isLazy: false, asset: $simpleAsset)]
),
$simpleAsset,
],
[
'app' => [
- 'path' => '/assets/app.js',
+ 'path' => '/assets/app-d1g3st.js',
'type' => 'js',
],
],
@@ -271,7 +299,7 @@ public function getRawImportMapDataTests(): iterable
yield 'it does not process dependencies of CSS files' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app.css',
path: 'app.css',
type: ImportMapType::CSS,
@@ -280,13 +308,13 @@ public function getRawImportMapDataTests(): iterable
[
new MappedAsset(
'app.css',
- publicPath: '/assets/app.css',
+ publicPath: '/assets/app-d1g3st.css',
javaScriptImports: [new JavaScriptImport('/assets/simple.js', asset: $simpleAsset)]
),
],
[
'app.css' => [
- 'path' => '/assets/app.css',
+ 'path' => '/assets/app-d1g3st.css',
'type' => 'css',
],
],
@@ -294,7 +322,7 @@ public function getRawImportMapDataTests(): iterable
yield 'it handles a relative path file' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
path: './assets/app.js',
),
@@ -304,12 +332,12 @@ public function getRawImportMapDataTests(): iterable
'app.js',
// /fake/root is the mocked root directory
'/fake/root/assets/app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
),
],
[
'app' => [
- 'path' => '/assets/app.js',
+ 'path' => '/assets/app-d1g3st.js',
'type' => 'js',
],
],
@@ -317,7 +345,7 @@ public function getRawImportMapDataTests(): iterable
yield 'it handles an absolute path file' => [
[
- new ImportMapEntry(
+ self::createLocalEntry(
'app',
path: '/some/path/assets/app.js',
),
@@ -326,12 +354,12 @@ public function getRawImportMapDataTests(): iterable
new MappedAsset(
'app.js',
'/some/path/assets/app.js',
- publicPath: '/assets/app.js',
+ publicPath: '/assets/app-d1g3st.js',
),
],
[
'app' => [
- 'path' => '/assets/app.js',
+ 'path' => '/assets/app-d1g3st.js',
'type' => 'js',
],
],
@@ -367,7 +395,7 @@ public function testGetEntrypointMetadata(MappedAsset $entryAsset, array $expect
$this->mockAssetMapper([$entryAsset]);
// put the entry asset in the importmap
$this->mockImportMap([
- new ImportMapEntry('the_entrypoint_name', path: $entryAsset->logicalPath, isEntrypoint: true),
+ ImportMapEntry::createLocal('the_entrypoint_name', ImportMapType::JS, path: $entryAsset->logicalPath, isEntrypoint: true),
]);
$this->assertEquals($expected, $manager->getEntrypointMetadata('the_entrypoint_name'));
@@ -448,31 +476,31 @@ public function testGetImportMapData()
{
$manager = $this->createImportMapManager();
$this->mockImportMap([
- new ImportMapEntry(
+ self::createLocalEntry(
'entry1',
path: 'entry1.js',
isEntrypoint: true,
),
- new ImportMapEntry(
+ self::createLocalEntry(
'entry2',
path: 'entry2.js',
isEntrypoint: true,
),
- new ImportMapEntry(
+ self::createLocalEntry(
'entry3',
path: 'entry3.js',
isEntrypoint: true,
),
- new ImportMapEntry(
+ self::createLocalEntry(
'normal_js_file',
path: 'normal_js_file.js',
),
- new ImportMapEntry(
+ self::createLocalEntry(
'css_in_importmap',
path: 'styles/css_in_importmap.css',
type: ImportMapType::CSS,
),
- new ImportMapEntry(
+ self::createLocalEntry(
'never_imported_css',
path: 'styles/never_imported_css.css',
type: ImportMapType::CSS,
@@ -631,7 +659,7 @@ public function testGetImportMapData()
public function testFindRootImportMapEntry()
{
$manager = $this->createImportMapManager();
- $entry1 = new ImportMapEntry('entry1', isEntrypoint: true);
+ $entry1 = ImportMapEntry::createLocal('entry1', ImportMapType::JS, '/any/path', isEntrypoint: true);
$this->mockImportMap([$entry1]);
$this->assertSame($entry1, $manager->findRootImportMapEntry('entry1'));
@@ -642,9 +670,9 @@ public function testGetEntrypointNames()
{
$manager = $this->createImportMapManager();
$this->mockImportMap([
- new ImportMapEntry('entry1', isEntrypoint: true),
- new ImportMapEntry('entry2', isEntrypoint: true),
- new ImportMapEntry('not_entrypoint'),
+ 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());
@@ -678,6 +706,7 @@ public function testRequire(array $packages, int $expectedProviderPackageArgumen
->method('getEntries')
->willReturn(new ImportMapEntries())
;
+
$this->configReader->expects($this->once())
->method('writeEntries')
->with($this->callback(function (ImportMapEntries $entries) use ($expectedImportMap) {
@@ -686,13 +715,14 @@ public function testRequire(array $packages, int $expectedProviderPackageArgumen
$simplifiedEntries = [];
foreach ($entries as $entry) {
$simplifiedEntries[$entry->importName] = [
- 'version' => $entry->version,
'path' => $entry->path,
'type' => $entry->type->value,
'entrypoint' => $entry->isEntrypoint,
- 'packageName' => $entry->packageName,
- 'filePath' => $entry->packageName,
];
+ if ($entry->isRemotePackage()) {
+ $simplifiedEntries[$entry->importName]['version'] = $entry->version;
+ $simplifiedEntries[$entry->importName]['packageModuleSpecifier'] = $entry->packageModuleSpecifier;
+ }
}
$this->assertSame(array_keys($expectedImportMap), array_keys($simplifiedEntries));
@@ -798,16 +828,11 @@ public function testRemove()
{
$manager = $this->createImportMapManager();
$this->mockImportMap([
- new ImportMapEntry('lodash', version: '1.2.3'),
- new ImportMapEntry('cowsay', version: '4.5.6'),
- new ImportMapEntry('chance', version: '7.8.9'),
- new ImportMapEntry('app', path: 'app.js'),
- new ImportMapEntry('other', path: 'other.js'),
- ]);
-
- $this->mockAssetMapper([
- new MappedAsset('vendor/moo.js', self::$writableRoot.'/assets/vendor/moo.js'),
- new MappedAsset('app.js', self::$writableRoot.'/assets/app.js'),
+ self::createRemoteEntry('lodash', version: '1.2.3', path: '/vendor/lodash.js'),
+ self::createRemoteEntry('cowsay', version: '4.5.6', path: '/vendor/cowsay.js'),
+ self::createRemoteEntry('chance', version: '7.8.9', path: '/vendor/chance.js'),
+ self::createLocalEntry('app', path: './app.js'),
+ self::createLocalEntry('other', path: './other.js'),
]);
$this->configReader->expects($this->once())
@@ -829,9 +854,9 @@ public function testUpdateAll()
{
$manager = $this->createImportMapManager();
$this->mockImportMap([
- new ImportMapEntry('lodash', version: '1.2.3'),
- new ImportMapEntry('bootstrap', version: '5.1.3'),
- new ImportMapEntry('app', path: 'app.js'),
+ self::createRemoteEntry('lodash', version: '1.2.3', path: '/vendor/lodash.js'),
+ self::createRemoteEntry('bootstrap', version: '5.1.3', path: '/vendor/bootstrap.js'),
+ self::createLocalEntry('app', path: 'app.js'),
]);
$this->packageResolver->expects($this->once())
@@ -841,8 +866,8 @@ public function testUpdateAll()
/* @var PackageRequireOptions[] $packages */
$this->assertCount(2, $packages);
- $this->assertSame('lodash', $packages[0]->packageName);
- $this->assertSame('bootstrap', $packages[1]->packageName);
+ $this->assertSame('lodash', $packages[0]->packageModuleSpecifier);
+ $this->assertSame('bootstrap', $packages[1]->packageModuleSpecifier);
return true;
}))
@@ -874,10 +899,10 @@ public function testUpdateWithSpecificPackages()
{
$manager = $this->createImportMapManager();
$this->mockImportMap([
- new ImportMapEntry('lodash', version: '1.2.3'),
- new ImportMapEntry('cowsay', version: '4.5.6'),
- new ImportMapEntry('bootstrap', version: '5.1.3'),
- new ImportMapEntry('app', path: 'app.js'),
+ self::createRemoteEntry('lodash', version: '1.2.3'),
+ self::createRemoteEntry('cowsay', version: '4.5.6'),
+ self::createRemoteEntry('bootstrap', version: '5.1.3'),
+ self::createLocalEntry('app', path: 'app.js'),
]);
$this->packageResolver->expects($this->once())
@@ -968,6 +993,15 @@ private function createImportMapManager(): ImportMapManager
$this->packageResolver = $this->createMock(PackageResolverInterface::class);
$this->remotePackageDownloader = $this->createMock(RemotePackageDownloader::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->importMapManager = new ImportMapManager(
$this->assetMapper,
$this->pathResolver,
@@ -997,7 +1031,7 @@ private function mockImportMap(array $importMapEntries): void
/**
* @param MappedAsset[] $mappedAssets
*/
- private function mockAssetMapper(array $mappedAssets, bool $mockGetAssetFromSourcePath = true): void
+ private function mockAssetMapper(array $mappedAssets): void
{
$this->assetMapper->expects($this->any())
->method('getAsset')
@@ -1012,10 +1046,6 @@ private function mockAssetMapper(array $mappedAssets, bool $mockGetAssetFromSour
})
;
- if (!$mockGetAssetFromSourcePath) {
- return;
- }
-
$this->assetMapper->expects($this->any())
->method('getAssetFromSourcePath')
->willReturnCallback(function (string $sourcePath) use ($mappedAssets) {
@@ -1051,25 +1081,6 @@ private function mockAssetMapper(array $mappedAssets, bool $mockGetAssetFromSour
;
}
- /**
- * @param ImportMapEntry[] $importMapEntries
- */
- private function mockDownloader(array $importMapEntries): void
- {
- $this->remotePackageDownloader->expects($this->any())
- ->method('getDownloadedPath')
- ->willReturnCallback(function (string $importName) use ($importMapEntries) {
- foreach ($importMapEntries as $entry) {
- if ($entry->importName === $importName) {
- return self::$writableRoot.'/assets/vendor/'.$importName.'.js';
- }
- }
-
- return null;
- })
- ;
- }
-
private function writeFile(string $filename, string $content): void
{
$path = \dirname(self::$writableRoot.'/'.$filename);
@@ -1078,4 +1089,17 @@ private function writeFile(string $filename, string $content): void
}
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);
+ }
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapUpdateCheckerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapUpdateCheckerTest.php
index c8ceb69987fa8..d134e9b8d7968 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapUpdateCheckerTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapUpdateCheckerTest.php
@@ -37,36 +37,38 @@ protected function setUp(): void
public function testGetAvailableUpdates()
{
$this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([
- '@hotwired/stimulus' => new ImportMapEntry(
+ '@hotwired/stimulus' => self::createRemoteEntry(
importName: '@hotwired/stimulus',
version: '3.2.1',
- packageName: '@hotwired/stimulus',
+ packageSpecifier: '@hotwired/stimulus',
),
- 'json5' => new ImportMapEntry(
+ 'json5' => self::createRemoteEntry(
importName: 'json5',
version: '1.0.0',
- packageName: 'json5',
+ packageSpecifier: 'json5',
),
- 'bootstrap' => new ImportMapEntry(
+ 'bootstrap' => self::createRemoteEntry(
importName: 'bootstrap',
version: '5.3.1',
- packageName: 'bootstrap',
+ packageSpecifier: 'bootstrap',
),
- 'bootstrap/dist/css/bootstrap.min.css' => new ImportMapEntry(
+ 'bootstrap/dist/css/bootstrap.min.css' => self::createRemoteEntry(
importName: 'bootstrap/dist/css/bootstrap.min.css',
version: '5.3.1',
type: ImportMapType::CSS,
- packageName: 'bootstrap',
+ packageSpecifier: 'bootstrap',
),
- 'lodash' => new ImportMapEntry(
+ 'lodash' => self::createRemoteEntry(
importName: 'lodash',
version: '4.17.21',
- packageName: 'lodash',
+ packageSpecifier: 'lodash',
),
// Local package won't appear in update list
- 'app' => new ImportMapEntry(
- importName: 'app',
- path: 'assets/app.js',
+ 'app' => ImportMapEntry::createLocal(
+ 'app',
+ ImportMapType::JS,
+ 'assets/app.js',
+ false,
),
]));
@@ -117,9 +119,9 @@ public function testGetAvailableUpdatesForSinglePackage(array $entries, array $e
$this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries($entries));
if (null !== $expectedException) {
$this->expectException($expectedException::class);
- $this->updateChecker->getAvailableUpdates(array_map(fn ($entry) => $entry->packageName, $entries));
+ $this->updateChecker->getAvailableUpdates(array_map(fn ($entry) => $entry->importName, $entries));
} else {
- $update = $this->updateChecker->getAvailableUpdates(array_map(fn ($entry) => $entry->packageName, $entries));
+ $update = $this->updateChecker->getAvailableUpdates(array_map(fn ($entry) => $entry->importName, $entries));
$this->assertEquals($expectedUpdateInfo, $update);
}
}
@@ -127,10 +129,10 @@ public function testGetAvailableUpdatesForSinglePackage(array $entries, array $e
private function provideImportMapEntry()
{
yield [
- ['@hotwired/stimulus' => new ImportMapEntry(
+ [self::createRemoteEntry(
importName: '@hotwired/stimulus',
version: '3.2.1',
- packageName: '@hotwired/stimulus',
+ packageSpecifier: '@hotwired/stimulus',
),
],
['@hotwired/stimulus' => new PackageUpdateInfo(
@@ -143,10 +145,10 @@ private function provideImportMapEntry()
];
yield [
[
- 'bootstrap/dist/css/bootstrap.min.css' => new ImportMapEntry(
+ self::createRemoteEntry(
importName: 'bootstrap/dist/css/bootstrap.min.css',
version: '5.3.1',
- packageName: 'bootstrap',
+ packageSpecifier: 'bootstrap',
),
],
['bootstrap/dist/css/bootstrap.min.css' => new PackageUpdateInfo(
@@ -159,10 +161,10 @@ private function provideImportMapEntry()
];
yield [
[
- 'bootstrap' => new ImportMapEntry(
+ self::createRemoteEntry(
importName: 'bootstrap',
version: 'not_a_version',
- packageName: 'bootstrap',
+ packageSpecifier: 'bootstrap',
),
],
[],
@@ -170,10 +172,10 @@ private function provideImportMapEntry()
];
yield [
[
- new ImportMapEntry(
+ self::createRemoteEntry(
importName: 'invalid_package_name',
version: '1.0.0',
- packageName: 'invalid_package_name',
+ packageSpecifier: 'invalid_package_name',
),
],
[],
@@ -201,4 +203,11 @@ private function responseFactory($method, $url): MockResponse
return $map[$url] ?? new MockResponse('Not found', ['http_code' => 404]);
}
+
+ private static function createRemoteEntry(string $importName, string $version, ImportMapType $type = ImportMapType::JS, string $packageSpecifier = null): ImportMapEntry
+ {
+ $packageSpecifier = $packageSpecifier ?? $importName;
+
+ return ImportMapEntry::createRemote($importName, $type, path: '/vendor/any-path.js', version: $version, packageModuleSpecifier: $packageSpecifier, isEntrypoint: false);
+ }
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php
index 2aaee06c01793..26c5ee8769d10 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php
@@ -17,6 +17,7 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
+use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface;
use Symfony\Component\Filesystem\Filesystem;
@@ -42,11 +43,13 @@ public function testDownloadPackagesDownloadsEverythingWithNoInstalled()
{
$configReader = $this->createMock(ImportMapConfigReader::class);
$packageResolver = $this->createMock(PackageResolverInterface::class);
+ $remotePackageStorage = new RemotePackageStorage(self::$writableRoot.'/assets/vendor');
- $entry1 = new ImportMapEntry('foo', version: '1.0.0');
- $entry2 = new ImportMapEntry('bar.js/file', version: '1.0.0');
- $entry3 = new ImportMapEntry('baz', version: '1.0.0', type: ImportMapType::CSS);
- $importMapEntries = new ImportMapEntries([$entry1, $entry2, $entry3]);
+ $entry1 = ImportMapEntry::createRemote('foo', ImportMapType::JS, path: '/any', version: '1.0.0', packageModuleSpecifier: 'foo', isEntrypoint: false);
+ $entry2 = ImportMapEntry::createRemote('bar.js/file', ImportMapType::JS, path: '/any', version: '1.0.0', packageModuleSpecifier: 'bar.js/file', isEntrypoint: false);
+ $entry3 = ImportMapEntry::createRemote('baz', ImportMapType::CSS, path: '/any', version: '1.0.0', packageModuleSpecifier: 'baz', isEntrypoint: false);
+ $entry4 = ImportMapEntry::createRemote('different_specifier', ImportMapType::JS, path: '/any', version: '1.0.0', packageModuleSpecifier: 'custom_specifier', isEntrypoint: false);
+ $importMapEntries = new ImportMapEntries([$entry1, $entry2, $entry3, $entry4]);
$configReader->expects($this->once())
->method('getEntries')
@@ -56,31 +59,33 @@ public function testDownloadPackagesDownloadsEverythingWithNoInstalled()
$packageResolver->expects($this->once())
->method('downloadPackages')
->with(
- ['foo' => $entry1, 'bar.js/file' => $entry2, 'baz' => $entry3],
+ ['foo' => $entry1, 'bar.js/file' => $entry2, 'baz' => $entry3, 'different_specifier' => $entry4],
$progressCallback
)
- ->willReturn(['foo' => 'foo content', 'bar.js/file' => 'bar content', 'baz' => 'baz content']);
+ ->willReturn(['foo' => 'foo content', 'bar.js/file' => 'bar content', 'baz' => 'baz content', 'different_specifier' => 'different content']);
$downloader = new RemotePackageDownloader(
+ $remotePackageStorage,
$configReader,
$packageResolver,
- self::$writableRoot.'/assets/vendor',
);
$downloader->downloadPackages($progressCallback);
- $this->assertFileExists(self::$writableRoot.'/assets/vendor/foo.js');
+ $this->assertFileExists(self::$writableRoot.'/assets/vendor/foo/foo.index.js');
$this->assertFileExists(self::$writableRoot.'/assets/vendor/bar.js/file.js');
- $this->assertFileExists(self::$writableRoot.'/assets/vendor/baz.css');
- $this->assertEquals('foo content', file_get_contents(self::$writableRoot.'/assets/vendor/foo.js'));
+ $this->assertFileExists(self::$writableRoot.'/assets/vendor/baz/baz.index.css');
+ $this->assertEquals('foo content', file_get_contents(self::$writableRoot.'/assets/vendor/foo/foo.index.js'));
$this->assertEquals('bar content', file_get_contents(self::$writableRoot.'/assets/vendor/bar.js/file.js'));
- $this->assertEquals('baz content', file_get_contents(self::$writableRoot.'/assets/vendor/baz.css'));
+ $this->assertEquals('baz content', file_get_contents(self::$writableRoot.'/assets/vendor/baz/baz.index.css'));
+ $this->assertEquals('different content', file_get_contents(self::$writableRoot.'/assets/vendor/custom_specifier/custom_specifier.index.js'));
$installed = require self::$writableRoot.'/assets/vendor/installed.php';
$this->assertEquals(
[
- 'foo' => ['path' => 'foo.js', 'version' => '1.0.0'],
- 'bar.js/file' => ['path' => 'bar.js/file.js', 'version' => '1.0.0'],
- 'baz' => ['path' => 'baz.css', 'version' => '1.0.0'],
+ 'foo' => ['version' => '1.0.0'],
+ 'bar.js/file' => ['version' => '1.0.0'],
+ 'baz' => ['version' => '1.0.0'],
+ 'different_specifier' => ['version' => '1.0.0'],
],
$installed
);
@@ -90,9 +95,9 @@ public function testPackagesWithCorrectInstalledVersionSkipped()
{
$this->filesystem->mkdir(self::$writableRoot.'/assets/vendor');
$installed = [
- 'foo' => ['path' => 'foo.js', 'version' => '1.0.0'],
- 'bar.js/file' => ['path' => 'bar.js/file.js', 'version' => '1.0.0'],
- 'baz' => ['path' => 'baz.css', 'version' => '1.0.0'],
+ 'foo' => ['version' => '1.0.0'],
+ 'bar.js/file' => ['version' => '1.0.0'],
+ 'baz' => ['version' => '1.0.0'],
];
file_put_contents(
self::$writableRoot.'/assets/vendor/installed.php',
@@ -103,13 +108,15 @@ public function testPackagesWithCorrectInstalledVersionSkipped()
$packageResolver = $this->createMock(PackageResolverInterface::class);
// matches installed version and file exists
- $entry1 = new ImportMapEntry('foo', version: '1.0.0');
- file_put_contents(self::$writableRoot.'/assets/vendor/foo.js', 'original foo content');
+ $entry1 = ImportMapEntry::createRemote('foo', ImportMapType::JS, path: '/any', version: '1.0.0', packageModuleSpecifier: 'foo', isEntrypoint: false);
+ @mkdir(self::$writableRoot.'/assets/vendor/foo', 0777, true);
+ file_put_contents(self::$writableRoot.'/assets/vendor/foo/foo.index.js', 'original foo content');
// matches installed version but file does not exist
- $entry2 = new ImportMapEntry('bar.js/file', version: '1.0.0');
+ $entry2 = ImportMapEntry::createRemote('bar.js/file', ImportMapType::JS, path: '/any', version: '1.0.0', packageModuleSpecifier: 'bar.js/file', isEntrypoint: false);
// does not match installed version
- $entry3 = new ImportMapEntry('baz', version: '1.1.0', type: ImportMapType::CSS);
- file_put_contents(self::$writableRoot.'/assets/vendor/baz.css', 'original baz content');
+ $entry3 = ImportMapEntry::createRemote('baz', ImportMapType::CSS, path: '/any', version: '1.1.0', packageModuleSpecifier: 'baz', isEntrypoint: false);
+ @mkdir(self::$writableRoot.'/assets/vendor/baz', 0777, true);
+ file_put_contents(self::$writableRoot.'/assets/vendor/baz/baz.index.css', 'original baz content');
$importMapEntries = new ImportMapEntries([$entry1, $entry2, $entry3]);
$configReader->expects($this->once())
@@ -121,57 +128,38 @@ public function testPackagesWithCorrectInstalledVersionSkipped()
->willReturn(['bar.js/file' => 'new bar content', 'baz' => 'new baz content']);
$downloader = new RemotePackageDownloader(
+ new RemotePackageStorage(self::$writableRoot.'/assets/vendor'),
$configReader,
$packageResolver,
- self::$writableRoot.'/assets/vendor',
);
$downloader->downloadPackages();
- $this->assertFileExists(self::$writableRoot.'/assets/vendor/foo.js');
+ $this->assertFileExists(self::$writableRoot.'/assets/vendor/foo/foo.index.js');
$this->assertFileExists(self::$writableRoot.'/assets/vendor/bar.js/file.js');
- $this->assertFileExists(self::$writableRoot.'/assets/vendor/baz.css');
- $this->assertEquals('original foo content', file_get_contents(self::$writableRoot.'/assets/vendor/foo.js'));
+ $this->assertFileExists(self::$writableRoot.'/assets/vendor/baz/baz.index.css');
+ $this->assertEquals('original foo content', file_get_contents(self::$writableRoot.'/assets/vendor/foo/foo.index.js'));
$this->assertEquals('new bar content', file_get_contents(self::$writableRoot.'/assets/vendor/bar.js/file.js'));
- $this->assertEquals('new baz content', file_get_contents(self::$writableRoot.'/assets/vendor/baz.css'));
+ $this->assertEquals('new baz content', file_get_contents(self::$writableRoot.'/assets/vendor/baz/baz.index.css'));
$installed = require self::$writableRoot.'/assets/vendor/installed.php';
$this->assertEquals(
[
- 'foo' => ['path' => 'foo.js', 'version' => '1.0.0'],
- 'bar.js/file' => ['path' => 'bar.js/file.js', 'version' => '1.0.0'],
- 'baz' => ['path' => 'baz.css', 'version' => '1.1.0'],
+ 'foo' => ['version' => '1.0.0'],
+ 'bar.js/file' => ['version' => '1.0.0'],
+ 'baz' => ['version' => '1.1.0'],
],
$installed
);
}
- public function testGetDownloadedPath()
- {
- $this->filesystem->mkdir(self::$writableRoot.'/assets/vendor');
- $installed = [
- 'foo' => ['path' => 'foo-path.js', 'version' => '1.0.0'],
- ];
- file_put_contents(
- self::$writableRoot.'/assets/vendor/installed.php',
- 'createMock(ImportMapConfigReader::class),
- $this->createMock(PackageResolverInterface::class),
- self::$writableRoot.'/assets/vendor',
- );
- $this->assertSame(realpath(self::$writableRoot.'/assets/vendor/foo-path.js'), realpath($downloader->getDownloadedPath('foo')));
- }
-
public function testGetVendorDir()
{
+ $remotePackageStorage = new RemotePackageStorage('/foo/assets/vendor');
$downloader = new RemotePackageDownloader(
+ $remotePackageStorage,
$this->createMock(ImportMapConfigReader::class),
$this->createMock(PackageResolverInterface::class),
- self::$writableRoot.'/assets/vendor',
);
- $this->assertSame(realpath(self::$writableRoot.'/assets/vendor'), realpath($downloader->getVendorDir()));
+ $this->assertSame('/foo/assets/vendor', $downloader->getVendorDir());
}
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php
new file mode 100644
index 0000000000000..724f24f124790
--- /dev/null
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageStorageTest.php
@@ -0,0 +1,100 @@
+
+ *
+ * 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\TestCase;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry;
+use Symfony\Component\AssetMapper\ImportMap\ImportMapType;
+use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
+use Symfony\Component\Filesystem\Filesystem;
+
+class RemotePackageStorageTest extends TestCase
+{
+ private Filesystem $filesystem;
+ private static string $writableRoot = __DIR__.'/../fixtures/importmaps_for_writing';
+
+ protected function setUp(): void
+ {
+ $this->filesystem = new Filesystem();
+ if (!file_exists(self::$writableRoot)) {
+ $this->filesystem->mkdir(self::$writableRoot);
+ }
+ }
+
+ protected function tearDown(): void
+ {
+ $this->filesystem->remove(self::$writableRoot);
+ }
+
+ public function testGetStorageDir()
+ {
+ $storage = new RemotePackageStorage(self::$writableRoot.'/assets/vendor');
+ $this->assertSame(realpath(self::$writableRoot.'/assets/vendor'), realpath($storage->getStorageDir()));
+ }
+
+ public function testIsDownloaded()
+ {
+ $storage = new RemotePackageStorage(self::$writableRoot.'/assets/vendor');
+ $entry = ImportMapEntry::createRemote('foo', ImportMapType::JS, '/does/not/matter', '1.0.0', 'module_specifier', false);
+ $this->assertFalse($storage->isDownloaded($entry));
+ $targetPath = self::$writableRoot.'/assets/vendor/module_specifier/module_specifier.index.js';
+ @mkdir(\dirname($targetPath), 0777, true);
+ file_put_contents($targetPath, 'any content');
+ $this->assertTrue($storage->isDownloaded($entry));
+ }
+
+ public function testSave()
+ {
+ $storage = new RemotePackageStorage(self::$writableRoot.'/assets/vendor');
+ $entry = ImportMapEntry::createRemote('foo', ImportMapType::JS, '/does/not/matter', '1.0.0', 'module_specifier', false);
+ $storage->save($entry, 'any content');
+ $targetPath = self::$writableRoot.'/assets/vendor/module_specifier/module_specifier.index.js';
+ $this->assertFileExists($targetPath);
+ $this->assertEquals('any content', file_get_contents($targetPath));
+ }
+
+ /**
+ * @dataProvider getDownloadPathTests
+ */
+ public function testGetDownloadedPath(string $packageModuleSpecifier, ImportMapType $importMapType, string $expectedPath)
+ {
+ $storage = new RemotePackageStorage(self::$writableRoot.'/assets/vendor');
+ $this->assertSame($expectedPath, $storage->getDownloadPath($packageModuleSpecifier, $importMapType));
+ }
+
+ public static function getDownloadPathTests()
+ {
+ yield 'javascript bare package' => [
+ 'packageModuleSpecifier' => 'foo',
+ 'importMapType' => ImportMapType::JS,
+ 'expectedPath' => self::$writableRoot.'/assets/vendor/foo/foo.index.js',
+ ];
+
+ yield 'javascript package with path' => [
+ 'packageModuleSpecifier' => 'foo/bar',
+ 'importMapType' => ImportMapType::JS,
+ 'expectedPath' => self::$writableRoot.'/assets/vendor/foo/bar.js',
+ ];
+
+ yield 'javascript package with path and extension' => [
+ 'packageModuleSpecifier' => 'foo/bar.js',
+ 'importMapType' => ImportMapType::JS,
+ 'expectedPath' => self::$writableRoot.'/assets/vendor/foo/bar.js',
+ ];
+
+ yield 'CSS package with path' => [
+ 'packageModuleSpecifier' => 'foo/bar',
+ 'importMapType' => ImportMapType::CSS,
+ 'expectedPath' => self::$writableRoot.'/assets/vendor/foo/bar.css',
+ ];
+ }
+}
diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php
index b27ff210f0448..fb29df4ad53e5 100644
--- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php
+++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php
@@ -47,9 +47,9 @@ public function testResolvePackages(array $packages, array $expectedRequests, ar
$actualResolvedPackages = $provider->resolvePackages($packages);
$this->assertCount(\count($expectedResolvedPackages), $actualResolvedPackages);
foreach ($actualResolvedPackages as $package) {
- $packageName = $package->requireOptions->packageName;
- $this->assertArrayHasKey($packageName, $expectedResolvedPackages);
- $this->assertSame($expectedResolvedPackages[$packageName]['version'], $package->version);
+ $importName = $package->requireOptions->importName;
+ $this->assertArrayHasKey($importName, $expectedResolvedPackages);
+ $this->assertSame($expectedResolvedPackages[$importName]['version'], $package->version);
}
$this->assertSame(\count($expectedRequests), $httpClient->getRequestsCount());
@@ -293,7 +293,20 @@ public function testDownloadPackages(array $importMapEntries, array $expectedReq
public static function provideDownloadPackagesTests()
{
yield 'single package' => [
- ['lodash' => new ImportMapEntry('lodash', version: '1.2.3', packageName: 'lodash')],
+ ['lodash' => self::createRemoteEntry('lodash', version: '1.2.3')],
+ [
+ [
+ 'url' => '/lodash@1.2.3/+esm',
+ 'body' => 'lodash contents',
+ ],
+ ],
+ [
+ 'lodash' => 'lodash contents',
+ ],
+ ];
+
+ yield 'importName differs from package specifier' => [
+ ['lodash' => self::createRemoteEntry('some_alias', version: '1.2.3', packageSpecifier: 'lodash')],
[
[
'url' => '/lodash@1.2.3/+esm',
@@ -306,7 +319,7 @@ public static function provideDownloadPackagesTests()
];
yield 'package with path' => [
- ['lodash' => new ImportMapEntry('chart.js/auto', version: '4.5.6', packageName: 'chart.js', filePath: '/auto')],
+ ['lodash' => self::createRemoteEntry('chart.js/auto', version: '4.5.6')],
[
[
'url' => '/chart.js@4.5.6/auto/+esm',
@@ -319,7 +332,7 @@ public static function provideDownloadPackagesTests()
];
yield 'css file' => [
- ['lodash' => new ImportMapEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS, packageName: 'bootstrap', filePath: '/dist/bootstrap.css')],
+ ['lodash' => self::createRemoteEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS)],
[
[
'url' => '/bootstrap@5.0.6/dist/bootstrap.css',
@@ -333,9 +346,9 @@ public static function provideDownloadPackagesTests()
yield 'multiple files' => [
[
- 'lodash' => new ImportMapEntry('lodash', version: '1.2.3', packageName: 'lodash'),
- 'chart.js/auto' => new ImportMapEntry('chart.js/auto', version: '4.5.6', packageName: 'chart.js', filePath: '/auto'),
- 'bootstrap/dist/bootstrap.css' => new ImportMapEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS, packageName: 'bootstrap', filePath: '/dist/bootstrap.css'),
+ 'lodash' => self::createRemoteEntry('lodash', version: '1.2.3'),
+ 'chart.js/auto' => self::createRemoteEntry('chart.js/auto', version: '4.5.6'),
+ 'bootstrap/dist/bootstrap.css' => self::createRemoteEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS),
],
[
[
@@ -360,7 +373,7 @@ public static function provideDownloadPackagesTests()
yield 'make imports relative' => [
[
- '@chart.js/auto' => new ImportMapEntry('chart.js/auto', version: '1.2.3', packageName: 'chart.js', filePath: '/auto'),
+ '@chart.js/auto' => self::createRemoteEntry('chart.js/auto', version: '1.2.3'),
],
[
[
@@ -375,7 +388,7 @@ public static function provideDownloadPackagesTests()
yield 'js importmap is removed' => [
[
- '@chart.js/auto' => new ImportMapEntry('chart.js/auto', version: '1.2.3', packageName: 'chart.js', filePath: '/auto'),
+ '@chart.js/auto' => self::createRemoteEntry('chart.js/auto', version: '1.2.3'),
],
[
[
@@ -390,7 +403,7 @@ public static function provideDownloadPackagesTests()
];
yield 'css file removes importmap' => [
- ['lodash' => new ImportMapEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS, packageName: 'bootstrap', filePath: '/dist/bootstrap.css')],
+ ['lodash' => self::createRemoteEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS)],
[
[
'url' => '/bootstrap@5.0.6/dist/bootstrap.css',
@@ -449,4 +462,11 @@ public static function provideImportRegex(): iterable
],
];
}
+
+ private static function createRemoteEntry(string $importName, string $version, ImportMapType $type = ImportMapType::JS, string $packageSpecifier = null): ImportMapEntry
+ {
+ $packageSpecifier = $packageSpecifier ?? $importName;
+
+ return ImportMapEntry::createRemote($importName, $type, path: 'does not matter', version: $version, packageModuleSpecifier: $packageSpecifier, isEntrypoint: false);
+ }
}
diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/AssetMapperTestAppKernel.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/AssetMapperTestAppKernel.php
index 03b8212518094..aed5e2457a753 100644
--- a/src/Symfony/Component/AssetMapper/Tests/fixtures/AssetMapperTestAppKernel.php
+++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/AssetMapperTestAppKernel.php
@@ -43,7 +43,7 @@ public function registerContainerConfiguration(LoaderInterface $loader): void
'http_client' => true,
'assets' => null,
'asset_mapper' => [
- 'paths' => ['dir1', 'dir2', 'assets/vendor'],
+ 'paths' => ['dir1', 'dir2', 'assets'],
],
'test' => true,
]);
diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/stimulus.js b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/@hotwired/stimulus/stimulus.index.js
similarity index 100%
rename from src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/stimulus.js
rename to src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/@hotwired/stimulus/stimulus.index.js
diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php
index 564dfcb1286d3..1d86382dcfc3f 100644
--- a/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php
+++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php
@@ -1,12 +1,10 @@
+ '@hotwired/stimulus' =>
array (
'version' => '3.2.1',
- 'path' => 'stimulus.js',
),
- 'lodash' =>
+ 'lodash' =>
array (
'version' => '4.17.21',
- 'path' => 'lodash.js',
),
-);
\ No newline at end of file
+);
diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/lodash.js b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/lodash/lodash.index.js
similarity index 100%
rename from src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/lodash.js
rename to src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/lodash/lodash.index.js