diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 3ba2ee8be2795..41f697c926f9d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -26,8 +26,9 @@ CHANGELOG * Deprecate `framework.validation.enable_annotations`, use `framework.validation.enable_attributes` instead * Deprecate `framework.serializer.enable_annotations`, use `framework.serializer.enable_attributes` instead * Add `array $tokenAttributes = []` optional parameter to `KernelBrowser::loginUser()` - * Add support for relative URLs in BrowserKit's redirect assertion. + * Add support for relative URLs in BrowserKit's redirect assertion * Change BrowserKitAssertionsTrait::getClient() to be protected + * Deprecate the `framework.asset_mapper.provider` config option 6.3 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index 2d1c8f041309b..047d30265fc74 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -25,7 +25,6 @@ class UnusedTagsPass implements CompilerPassInterface 'annotations.cached_reader', 'assets.package', 'asset_mapper.compiler', - 'asset_mapper.importmap.resolver', 'auto_alias', 'cache.pool', 'cache.pool.clearer', diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 795eda5d74122..4410181f9127b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -18,7 +18,6 @@ use Symfony\Bundle\FullStack; use Symfony\Component\Asset\Package; use Symfony\Component\AssetMapper\AssetMapper; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\NodeBuilder; @@ -940,8 +939,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $ ->defaultValue('%kernel.project_dir%/assets/vendor') ->end() ->scalarNode('provider') - ->info('The provider (CDN) to use'.(class_exists(ImportMapManager::class) ? sprintf(' (e.g.: "%s").', implode('", "', ImportMapManager::PROVIDERS)) : '.')) - ->defaultValue('jsdelivr.esm') + ->setDeprecated('symfony/framework-bundle', '6.4', 'Option "%node%" at "%path%" is deprecated and does nothing. Remove it.') ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index c6b8641f504aa..4b8e5a8ba0c59 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -34,7 +34,6 @@ use Symfony\Component\AssetMapper\AssetMapper; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; -use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\BrowserKit\AbstractBrowser; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; @@ -1364,18 +1363,17 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde ->setArgument(1, $config['missing_import_mode']); $container - ->getDefinition('asset_mapper.importmap.manager') - ->replaceArgument(3, $config['vendor_dir']) + ->getDefinition('asset_mapper.importmap.remote_package_downloader') + ->replaceArgument(2, $config['vendor_dir']) ; - $container - ->getDefinition('asset_mapper.importmap.config_reader') - ->replaceArgument(0, $config['importmap_path']) + ->getDefinition('asset_mapper.mapped_asset_factory') + ->replaceArgument(2, $config['vendor_dir']) ; $container - ->getDefinition('asset_mapper.importmap.resolver') - ->replaceArgument(0, $config['provider']) + ->getDefinition('asset_mapper.importmap.config_reader') + ->replaceArgument(0, $config['importmap_path']) ; $container @@ -1383,9 +1381,6 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde ->replaceArgument(3, $config['importmap_polyfill'] ?? ImportMapManager::POLYFILL_URL) ->replaceArgument(4, $config['importmap_script_attributes']) ; - - $container->registerForAutoconfiguration(PackageResolverInterface::class) - ->addTag('asset_mapper.importmap.resolver'); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php index 624bdef4db4db..d15dd70c93498 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php @@ -32,9 +32,8 @@ use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer; +use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader; use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver; -use Symfony\Component\AssetMapper\ImportMap\Resolver\JspmResolver; -use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolver; use Symfony\Component\AssetMapper\MapperAwareAssetPackage; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver; use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -53,6 +52,7 @@ ->args([ service('asset_mapper.public_assets_path_resolver'), service('asset_mapper_compiler'), + abstract_arg('vendor directory'), ]) ->set('asset_mapper.cached_mapped_asset_factory', CachedMappedAssetFactory::class) @@ -150,41 +150,20 @@ service('asset_mapper'), service('asset_mapper.public_assets_path_resolver'), service('asset_mapper.importmap.config_reader'), - abstract_arg('vendor directory'), + service('asset_mapper.importmap.remote_package_downloader'), service('asset_mapper.importmap.resolver'), - service('http_client'), ]) ->alias(ImportMapManager::class, 'asset_mapper.importmap.manager') - ->set('asset_mapper.importmap.resolver', PackageResolver::class) + ->set('asset_mapper.importmap.remote_package_downloader', RemotePackageDownloader::class) ->args([ - abstract_arg('provider'), - tagged_locator('asset_mapper.importmap.resolver'), + service('asset_mapper.importmap.config_reader'), + service('asset_mapper.importmap.resolver'), + abstract_arg('vendor directory'), ]) - ->set('asset_mapper.importmap.resolver.jsdelivr_esm', JsDelivrEsmResolver::class) + ->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class) ->args([service('http_client')]) - ->tag('asset_mapper.importmap.resolver', ['resolver' => ImportMapManager::PROVIDER_JSDELIVR_ESM]) - - ->set('asset_mapper.importmap.resolver.jspm', JspmResolver::class) - ->args([service('http_client'), ImportMapManager::PROVIDER_JSPM]) - ->tag('asset_mapper.importmap.resolver', ['resolver' => ImportMapManager::PROVIDER_JSPM]) - - ->set('asset_mapper.importmap.resolver.jspm_system', JspmResolver::class) - ->args([service('http_client'), ImportMapManager::PROVIDER_JSPM_SYSTEM]) - ->tag('asset_mapper.importmap.resolver', ['resolver' => ImportMapManager::PROVIDER_JSPM_SYSTEM]) - - ->set('asset_mapper.importmap.resolver.skypack', JspmResolver::class) - ->args([service('http_client'), ImportMapManager::PROVIDER_SKYPACK]) - ->tag('asset_mapper.importmap.resolver', ['resolver' => ImportMapManager::PROVIDER_SKYPACK]) - - ->set('asset_mapper.importmap.resolver.jsdelivr', JspmResolver::class) - ->args([service('http_client'), ImportMapManager::PROVIDER_JSDELIVR]) - ->tag('asset_mapper.importmap.resolver', ['resolver' => ImportMapManager::PROVIDER_JSDELIVR]) - - ->set('asset_mapper.importmap.resolver.unpkg', JspmResolver::class) - ->args([service('http_client'), ImportMapManager::PROVIDER_UNPKG]) - ->tag('asset_mapper.importmap.resolver', ['resolver' => ImportMapManager::PROVIDER_UNPKG]) ->set('asset_mapper.importmap.renderer', ImportMapRenderer::class) ->args([ @@ -198,14 +177,12 @@ ->set('asset_mapper.importmap.auditor', ImportMapAuditor::class) ->args([ service('asset_mapper.importmap.config_reader'), - service('asset_mapper.importmap.resolver'), service('http_client'), ]) ->set('asset_mapper.importmap.command.require', ImportMapRequireCommand::class) ->args([ service('asset_mapper.importmap.manager'), - service('asset_mapper'), param('kernel.project_dir'), ]) ->tag('console.command') @@ -219,7 +196,10 @@ ->tag('console.command') ->set('asset_mapper.importmap.command.install', ImportMapInstallCommand::class) - ->args([service('asset_mapper.importmap.manager')]) + ->args([ + service('asset_mapper.importmap.remote_package_downloader'), + param('kernel.project_dir'), + ]) ->tag('console.command') ->set('asset_mapper.importmap.command.audit', ImportMapAuditCommand::class) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 55020f78cf655..1e251bbd4480f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -134,7 +134,6 @@ public function testAssetMapperCanBeEnabled() 'importmap_path' => '%kernel.project_dir%/importmap.php', 'importmap_polyfill' => null, 'vendor_dir' => '%kernel.project_dir%/assets/vendor', - 'provider' => 'jsdelivr.esm', 'importmap_script_attributes' => [], ]; @@ -671,7 +670,6 @@ protected static function getBundleDefaultConfig() 'importmap_path' => '%kernel.project_dir%/importmap.php', 'importmap_polyfill' => null, 'vendor_dir' => '%kernel.project_dir%/assets/vendor', - 'provider' => 'jsdelivr.esm', 'importmap_script_attributes' => [], ], 'cache' => [ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/asset_mapper.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/asset_mapper.xml index 62351c93bb7e4..8007170ce912c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/asset_mapper.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/asset_mapper.xml @@ -17,7 +17,6 @@ importmap-path="%kernel.project_dir%/importmap.php" importmap-polyfill="https://cdn.example.com/polyfill.js" vendor-dir="%kernel.project_dir%/assets/vendor" - provider="jspm" > assets/ assets2/ diff --git a/src/Symfony/Component/AssetMapper/AssetMapperRepository.php b/src/Symfony/Component/AssetMapper/AssetMapperRepository.php index 17986d88d61bf..eb9e20506baa4 100644 --- a/src/Symfony/Component/AssetMapper/AssetMapperRepository.php +++ b/src/Symfony/Component/AssetMapper/AssetMapperRepository.php @@ -103,6 +103,7 @@ public function all(): array foreach ($this->getDirectories() as $path => $namespace) { $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); foreach ($iterator as $file) { + /** @var \SplFileInfo $file */ if (!$file->isFile()) { continue; } @@ -111,6 +112,11 @@ public function all(): array continue; } + // avoid potentially exposing PHP files + if ('php' === $file->getExtension()) { + continue; + } + /** @var RecursiveDirectoryIterator $innerIterator */ $innerIterator = $iterator->getInnerIterator(); $logicalPath = ($namespace ? rtrim($namespace, '/').'/' : '').$innerIterator->getSubPathName(); diff --git a/src/Symfony/Component/AssetMapper/CHANGELOG.md b/src/Symfony/Component/AssetMapper/CHANGELOG.md index d53aff3233b93..82867ece4a332 100644 --- a/src/Symfony/Component/AssetMapper/CHANGELOG.md +++ b/src/Symfony/Component/AssetMapper/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Mark the component as non experimental * Add CSS support to the importmap * Add "entrypoints" concept to the importmap + * Always download packages locally instead of using a CDN * Allow relative path strings in the importmap * Add `PreAssetsCompileEvent` event when running `asset-map:compile` * Add support for importmap paths to use the Asset component (for subdirectories) diff --git a/src/Symfony/Component/AssetMapper/Command/ImportMapInstallCommand.php b/src/Symfony/Component/AssetMapper/Command/ImportMapInstallCommand.php index 6924deddc55ca..2370eb610bb6d 100644 --- a/src/Symfony/Component/AssetMapper/Command/ImportMapInstallCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/ImportMapInstallCommand.php @@ -11,12 +11,14 @@ namespace Symfony\Component\AssetMapper\Command; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; +use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Contracts\HttpClient\ResponseInterface; /** * Downloads all assets that should be downloaded. @@ -27,7 +29,8 @@ final class ImportMapInstallCommand extends Command { public function __construct( - private readonly ImportMapManager $importMapManager, + private readonly RemotePackageDownloader $packageDownloader, + private readonly string $projectDir, ) { parent::__construct(); } @@ -36,8 +39,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $downloadedPackages = $this->importMapManager->downloadMissingPackages(); - $io->success(sprintf('Downloaded %d assets.', \count($downloadedPackages))); + $finishedCount = 0; + $progressBar = new ProgressBar($output); + $progressBar->setFormat('%current%/%max% %bar% %url%'); + $downloadedPackages = $this->packageDownloader->downloadPackages(function (string $package, string $event, ResponseInterface $response, int $totalPackages) use (&$finishedCount, $progressBar) { + $progressBar->setMessage($response->getInfo('url'), 'url'); + if (0 === $progressBar->getMaxSteps()) { + $progressBar->setMaxSteps($totalPackages); + $progressBar->start(); + } + + if ('finished' === $event) { + ++$finishedCount; + $progressBar->advance(); + } + }); + $progressBar->finish(); + $progressBar->clear(); + + if (!$downloadedPackages) { + $io->success('No assets to install.'); + + return Command::SUCCESS; + } + + $io->success(sprintf( + 'Downloaded %d asset%s into %s.', + \count($downloadedPackages), + 1 === \count($downloadedPackages) ? '' : 's', + str_replace($this->projectDir.'/', '', $this->packageDownloader->getVendorDir()), + )); return Command::SUCCESS; } diff --git a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php index 46c9ddbe88c45..17c6ab3ee33a2 100644 --- a/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php +++ b/src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php @@ -11,8 +11,6 @@ namespace Symfony\Component\AssetMapper\Command; -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; @@ -32,7 +30,6 @@ final class ImportMapRequireCommand extends Command { public function __construct( private readonly ImportMapManager $importMapManager, - private readonly AssetMapperInterface $assetMapper, private readonly string $projectDir, ) { parent::__construct(); @@ -42,7 +39,7 @@ protected function configure(): void { $this ->addArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The packages to add') - ->addOption('download', 'd', InputOption::VALUE_NONE, 'Download packages locally') + ->addOption('entrypoint', null, InputOption::VALUE_NONE, 'Make the package(s) an entrypoint?') ->addOption('path', null, InputOption::VALUE_REQUIRED, 'The local path where the package lives relative to the project root') ->setHelp(<<<'EOT' The %command.name% command adds packages to importmap.php usually @@ -113,10 +110,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $packages[] = new PackageRequireOptions( $parts['package'], $parts['version'] ?? null, - $input->getOption('download'), $parts['alias'] ?? $parts['package'], - isset($parts['registry']) && $parts['registry'] ? $parts['registry'] : null, $path, + $input->getOption('entrypoint'), ); } @@ -125,19 +121,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int $newPackage = $newPackages[0]; $message = sprintf('Package "%s" added to importmap.php', $newPackage->importName); - if ($newPackage->isDownloaded && null !== $downloadedAsset = $this->assetMapper->getAsset($newPackage->path)) { - $application = $this->getApplication(); - if ($application instanceof Application) { - $projectDir = $application->getKernel()->getProjectDir(); - $downloadedPath = $downloadedAsset->sourcePath; - if (str_starts_with($downloadedPath, $projectDir)) { - $downloadedPath = substr($downloadedPath, \strlen($projectDir) + 1); - } - - $message .= sprintf(' and downloaded locally to "%s"', $downloadedPath); - } - } - $message .= '.'; } else { $names = array_map(fn (ImportMapEntry $package) => $package->importName, $newPackages); diff --git a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php index 0ad27757a148f..147d63a40b82c 100644 --- a/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php +++ b/src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php @@ -146,7 +146,7 @@ private function findAssetForBareImport(string $importedModule, AssetMapperInter } // remote entries have no MappedAsset - if ($importMapEntry->isRemote()) { + if ($importMapEntry->isRemotePackage()) { return null; } @@ -158,7 +158,10 @@ private function findAssetForRelativeImport(string $importedModule, MappedAsset try { $resolvedPath = $this->resolvePath(\dirname($asset->logicalPath), $importedModule); } catch (RuntimeException $e) { - $this->handleMissingImport(sprintf('Error processing import in "%s": ', $asset->sourcePath).$e->getMessage(), $e); + // avoid warning about vendor imports - these are often comments + if (!$asset->isVendor) { + $this->handleMissingImport(sprintf('Error processing import in "%s": ', $asset->sourcePath).$e->getMessage(), $e); + } return null; } @@ -179,7 +182,10 @@ private function findAssetForRelativeImport(string $importedModule, MappedAsset // avoid circular error if there is self-referencing import comments } - $this->handleMissingImport($message); + // avoid warning about vendor imports - these are often comments + if (!$asset->isVendor) { + $this->handleMissingImport($message); + } return null; } diff --git a/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php b/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php index 9c1de8ab997bb..85c1f8ac38fa8 100644 --- a/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php +++ b/src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php @@ -29,8 +29,9 @@ class MappedAssetFactory implements MappedAssetFactoryInterface private array $fileContentsCache = []; public function __construct( - private PublicAssetsPathResolverInterface $assetsPathResolver, - private AssetMapperCompiler $compiler, + private readonly PublicAssetsPathResolverInterface $assetsPathResolver, + private readonly AssetMapperCompiler $compiler, + private readonly string $vendorDir, ) { } @@ -43,7 +44,8 @@ public function createMappedAsset(string $logicalPath, string $sourcePath): ?Map if (!isset($this->assetsCache[$logicalPath])) { $this->assetsBeingCreated[] = $logicalPath; - $asset = new MappedAsset($logicalPath, $sourcePath, $this->assetsPathResolver->resolvePublicPath($logicalPath)); + $isVendor = $this->isVendor($sourcePath); + $asset = new MappedAsset($logicalPath, $sourcePath, $this->assetsPathResolver->resolvePublicPath($logicalPath), isVendor: $isVendor); [$digest, $isPredigested] = $this->getDigest($asset); @@ -55,6 +57,7 @@ public function createMappedAsset(string $logicalPath, string $sourcePath): ?Map $this->calculateContent($asset), $digest, $isPredigested, + $isVendor, $asset->getDependencies(), $asset->getFileDependencies(), $asset->getJavaScriptImports(), @@ -116,4 +119,12 @@ private function getPublicPath(MappedAsset $asset): ?string return $this->assetsPathResolver->resolvePublicPath($digestedPath); } + + private function isVendor(string $sourcePath): bool + { + $sourcePath = realpath($sourcePath); + $vendorDir = realpath($this->vendorDir); + + return $sourcePath && str_starts_with($sourcePath, $vendorDir); + } } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php index 0f39e215381c9..1d49e0c77055b 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php @@ -12,7 +12,6 @@ namespace Symfony\Component\AssetMapper\ImportMap; use Symfony\Component\AssetMapper\Exception\RuntimeException; -use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\HttpClient\HttpClient; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -24,7 +23,6 @@ class ImportMapAuditor public function __construct( private readonly ImportMapConfigReader $configReader, - private readonly PackageResolverInterface $packageResolver, HttpClientInterface $httpClient = null, ) { $this->httpClient = $httpClient ?? HttpClient::create(); @@ -48,10 +46,10 @@ public function audit(): array $installed = []; $affectsQuery = []; foreach ($entries as $entry) { - if (null === $entry->url) { + if (!$entry->isRemotePackage()) { continue; } - $version = $entry->version ?? $this->packageResolver->getPackageVersion($entry->url); + $version = $entry->version; $installed[$entry->importName] ??= []; $installed[$entry->importName][] = $version; diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php index 880e3c5381827..ca77b1cd6a0a8 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php @@ -38,11 +38,16 @@ public function getEntries(): ImportMapEntries $entries = new ImportMapEntries(); foreach ($importMapConfig ?? [] as $importName => $data) { - $validKeys = ['path', 'url', 'downloaded_to', 'type', 'entrypoint', 'version']; + $validKeys = ['path', 'version', 'type', 'entrypoint', 'url']; 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))); } + // should solve itself when the config is written again + if (isset($data['url'])) { + trigger_deprecation('symfony/asset-mapper', '6.4', 'The "url" option is deprecated, use "version" instead.'); + } + $type = isset($data['type']) ? ImportMapType::tryFrom($data['type']) : ImportMapType::JS; $isEntry = $data['entrypoint'] ?? false; @@ -50,14 +55,25 @@ public function getEntries(): ImportMapEntries 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)); } + $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) { + 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)); + } + $entries->add(new ImportMapEntry( $importName, - path: $data['path'] ?? $data['downloaded_to'] ?? null, - url: $data['url'] ?? null, - isDownloaded: isset($data['downloaded_to']), + path: $path, + version: $version, type: $type, isEntrypoint: $isEntry, - version: $data['version'] ?? null, )); } @@ -73,10 +89,10 @@ public function writeEntries(ImportMapEntries $entries): void $config = []; if ($entry->path) { $path = $entry->path; - $config[$entry->isDownloaded ? 'downloaded_to' : 'path'] = $path; + $config['path'] = $path; } - if ($entry->url) { - $config['url'] = $entry->url; + if ($entry->version) { + $config['version'] = $entry->version; } if (ImportMapType::JS !== $entry->type) { $config['type'] = $entry->type->value; @@ -84,9 +100,6 @@ public function writeEntries(ImportMapEntries $entries): void if ($entry->isEntrypoint) { $config['entrypoint'] = true; } - if ($entry->version) { - $config['version'] = $entry->version; - } $importMapConfig[$entry->importName] = $config; } @@ -116,4 +129,19 @@ public function getRootDirectory(): string { return \dirname($this->importMapConfigPath); } + + private function extractVersionFromLegacyUrl(string $url): ?string + { + // URL pattern https://ga.jspm.io/npm:bootstrap@5.3.2/dist/js/bootstrap.esm.js + if (false === $lastAt = strrpos($url, '@')) { + return null; + } + + $nextSlash = strpos($url, '/', $lastAt); + if (false === $nextSlash) { + return null; + } + + return substr($url, $lastAt + 1, $nextSlash - $lastAt - 1); + } } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php index ee201585f5063..51e201cc1094d 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapEntry.php @@ -24,16 +24,14 @@ public function __construct( * The path to the asset if local or downloaded. */ public readonly ?string $path = null, - public readonly ?string $url = null, - public readonly bool $isDownloaded = false, + public readonly ?string $version = null, public readonly ImportMapType $type = ImportMapType::JS, public readonly bool $isEntrypoint = false, - public readonly ?string $version = null, ) { } - public function isRemote(): bool + public function isRemotePackage(): bool { - return (bool) $this->url; + return null !== $this->version; } } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php index 212f1cdb76602..3feb5eb9f1663 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/ImportMapManager.php @@ -15,8 +15,6 @@ use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; -use Symfony\Component\HttpClient\HttpClient; -use Symfony\Contracts\HttpClient\HttpClientInterface; /** * @author Kévin Dunglas @@ -26,43 +24,17 @@ */ class ImportMapManager { - public const PROVIDER_JSPM = 'jspm'; - public const PROVIDER_JSPM_SYSTEM = 'jspm.system'; - public const PROVIDER_SKYPACK = 'skypack'; - public const PROVIDER_JSDELIVR = 'jsdelivr'; - public const PROVIDER_JSDELIVR_ESM = 'jsdelivr.esm'; - public const PROVIDER_UNPKG = 'unpkg'; - public const PROVIDERS = [ - self::PROVIDER_JSPM, - self::PROVIDER_JSPM_SYSTEM, - self::PROVIDER_SKYPACK, - self::PROVIDER_JSDELIVR, - self::PROVIDER_JSDELIVR_ESM, - self::PROVIDER_UNPKG, - ]; - public const POLYFILL_URL = 'https://ga.jspm.io/npm:es-module-shims@1.7.2/dist/es-module-shims.js'; - - /** - * @see https://regex101.com/r/2cR9Rh/1 - * - * Partially based on https://github.com/dword-design/package-name-regex - */ - private const PACKAGE_PATTERN = '/^(?:https?:\/\/[\w\.-]+\/)?(?:(?\w+):)?(?(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*)(?:@(?[\w\._-]+))?(?:(?\/.*))?$/'; public const IMPORT_MAP_CACHE_FILENAME = 'importmap.json'; public const ENTRYPOINT_CACHE_FILENAME_PATTERN = 'entrypoint.%s.json'; - private readonly HttpClientInterface $httpClient; - public function __construct( private readonly AssetMapperInterface $assetMapper, private readonly PublicAssetsPathResolverInterface $assetsPathResolver, private readonly ImportMapConfigReader $importMapConfigReader, - private readonly string $vendorDir, + private readonly RemotePackageDownloader $packageDownloader, private readonly PackageResolverInterface $resolver, - HttpClientInterface $httpClient = null, ) { - $this->httpClient = $httpClient ?? HttpClient::create(); } /** @@ -97,33 +69,6 @@ public function update(array $packages = []): array return $this->updateImportMapConfig(true, [], [], $packages); } - /** - * Downloads all missing downloaded packages. - * - * @return string[] The downloaded packages - */ - public function downloadMissingPackages(): array - { - $entries = $this->importMapConfigReader->getEntries(); - $downloadedPackages = []; - - foreach ($entries as $entry) { - if (!$entry->isDownloaded || $this->findAsset($entry->path)) { - continue; - } - - $this->downloadPackage( - $entry->importName, - $this->httpClient->request('GET', $entry->url)->getContent(), - self::getImportMapTypeFromFilename($entry->url), - ); - - $downloadedPackages[] = $entry->importName; - } - - return $downloadedPackages; - } - public function findRootImportMapEntry(string $moduleName): ?ImportMapEntry { $entries = $this->importMapConfigReader->getEntries(); @@ -214,18 +159,18 @@ public function getRawImportMapData(): array $asset = $this->findAsset($entry->path); if (!$asset) { - if ($entry->isDownloaded) { - throw new \InvalidArgumentException(sprintf('The "%s" downloaded asset is missing. Run "php bin/console importmap:install".', $entry->path)); - } - throw new \InvalidArgumentException(sprintf('The asset "%s" cannot be found in any asset map paths.', $entry->path)); } - - $path = $asset->publicPath; } else { - $path = $entry->url; + $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)); + } } + $path = $asset->publicPath; $data = ['path' => $path, 'type' => $entry->type->value]; $rawImportMapData[$entry->importName] = $data; } @@ -238,8 +183,8 @@ public function getRawImportMapData(): array */ public static function parsePackageName(string $packageName): ?array { - // https://regex101.com/r/MDz0bN/1 - $regex = '/(?:(?P[^:\n]+):)?((?P@?[^=@\n]+))(?:@(?P[^=\s\n]+))?(?:=(?P[^\s\n]+))?/'; + // https://regex101.com/r/z1nj7P/1 + $regex = '/((?P@?[^=@\n]+))(?:@(?P[^=\s\n]+))?(?:=(?P[^\s\n]+))?/'; if (!preg_match($regex, $packageName, $matches)) { return null; @@ -274,27 +219,18 @@ private function updateImportMapConfig(bool $update, array $packagesToRequire, a if ($update) { foreach ($currentEntries as $entry) { $importName = $entry->importName; - if (null === $entry->url || (0 !== \count($packagesToUpdate) && !\in_array($importName, $packagesToUpdate, true))) { + if (!$entry->isRemotePackage() || ($packagesToUpdate && !\in_array($importName, $packagesToUpdate, true))) { continue; } // assume the import name === package name, unless we can parse // the true package name from the URL $packageName = $importName; - $registry = null; - - // try to grab the package name & jspm "registry" from the URL - if (str_starts_with($entry->url, 'https://ga.jspm.io') && 1 === preg_match(self::PACKAGE_PATTERN, $entry->url, $matches)) { - $packageName = $matches['package']; - $registry = $matches['registry'] ?? null; - } $packagesToRequire[] = new PackageRequireOptions( $packageName, null, - $entry->isDownloaded, $importName, - $registry, ); // remove it: then it will be re-added @@ -305,6 +241,7 @@ private function updateImportMapConfig(bool $update, array $packagesToRequire, a $newEntries = $this->requirePackages($packagesToRequire, $currentEntries); $this->importMapConfigReader->writeEntries($currentEntries); + $this->packageDownloader->downloadPackages(); return $newEntries; } @@ -345,6 +282,7 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp $requireOptions->packageName, path: $path, type: self::getImportMapTypeFromFilename($requireOptions->path), + isEntrypoint: $requireOptions->entrypoint, ); $importMapEntries->add($newEntry); $addedEntries[] = $newEntry; @@ -358,22 +296,13 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp $resolvedPackages = $this->resolver->resolvePackages($packagesToRequire); foreach ($resolvedPackages as $resolvedPackage) { $importName = $resolvedPackage->requireOptions->importName ?: $resolvedPackage->requireOptions->packageName; - $path = null; - $type = self::getImportMapTypeFromFilename($resolvedPackage->url); - if ($resolvedPackage->requireOptions->download) { - if (null === $resolvedPackage->content) { - throw new \LogicException(sprintf('The contents of package "%s" were not downloaded.', $resolvedPackage->requireOptions->packageName)); - } - - $path = $this->downloadPackage($importName, $resolvedPackage->content, $type); - } $newEntry = new ImportMapEntry( $importName, - path: $path, - url: $resolvedPackage->url, - isDownloaded: $resolvedPackage->requireOptions->download, - type: $type, + path: $resolvedPackage->requireOptions->path, + version: $resolvedPackage->version, + type: $resolvedPackage->type, + isEntrypoint: $resolvedPackage->requireOptions->entrypoint, ); $importMapEntries->add($newEntry); $addedEntries[] = $newEntry; @@ -418,7 +347,7 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE } // remote packages aren't in the asset mapper & so don't have dependencies - if ($entry->isRemote()) { + if ($entry->isRemotePackage()) { return $currentImportEntries; } @@ -457,26 +386,6 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE return $currentImportEntries; } - private function downloadPackage(string $packageName, string $packageContents, ImportMapType $importMapType): string - { - $vendorPath = $this->vendorDir.'/'.$packageName; - // add an extension of there is none - if (!str_contains($packageName, '.')) { - $vendorPath .= '.'.$importMapType->value; - } - - @mkdir(\dirname($vendorPath), 0777, true); - file_put_contents($vendorPath, $packageContents); - - if (null === $mappedAsset = $this->assetMapper->getAssetFromSourcePath($vendorPath)) { - unlink($vendorPath); - - throw new \LogicException(sprintf('The package was downloaded to "%s", but this path does not appear to be in any of your asset paths.', $vendorPath)); - } - - return $mappedAsset->logicalPath; - } - /** * Given an importmap entry name, finds all the non-lazy module imports in its chain. * @@ -498,7 +407,7 @@ private function findEagerEntrypointImports(string $entryName): array throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is not an entry point in "importmap.php". Set "entrypoint" => true to make it available as an entrypoint.', $entryName)); } - if ($rootImportEntries->get($entryName)->isRemote()) { + if ($rootImportEntries->get($entryName)->isRemotePackage()) { throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is a remote package and cannot be used as an entrypoint.', $entryName)); } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php b/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php index be02902174065..095533c69f07c 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/PackageRequireOptions.php @@ -21,10 +21,9 @@ final class PackageRequireOptions public function __construct( public readonly string $packageName, public readonly ?string $versionConstraint = null, - public readonly bool $download = false, public readonly ?string $importName = null, - public readonly ?string $registryName = null, public readonly ?string $path = null, + public readonly bool $entrypoint = false, ) { } } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php b/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php new file mode 100644 index 0000000000000..a3440473ab792 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/ImportMap/RemotePackageDownloader.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AssetMapper\ImportMap; + +use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; + +/** + * @final + */ +class RemotePackageDownloader +{ + private array $installed; + + public function __construct( + private readonly ImportMapConfigReader $importMapConfigReader, + private readonly PackageResolverInterface $packageResolver, + private readonly string $vendorDir, + ) { + } + + /** + * Downloads all packages. + * + * @return string[] The downloaded packages + */ + public function downloadPackages(callable $progressCallback = null): array + { + try { + $installed = $this->loadInstalled(); + } catch (\InvalidArgumentException) { + $installed = []; + } + $entries = $this->importMapConfigReader->getEntries(); + $remoteEntriesToDownload = []; + $newInstalled = []; + foreach ($entries as $entry) { + if (!$entry->isRemotePackage()) { + continue; + } + + // if the file exists at the correct version, skip it + if ( + isset($installed[$entry->importName]) + && $installed[$entry->importName]['version'] === $entry->version + && file_exists($this->vendorDir.'/'.$installed[$entry->importName]['path']) + ) { + $newInstalled[$entry->importName] = $installed[$entry->importName]; + continue; + } + + $remoteEntriesToDownload[$entry->importName] = $entry; + } + + if (!$remoteEntriesToDownload) { + return []; + } + + $contents = $this->packageResolver->downloadPackages($remoteEntriesToDownload, $progressCallback); + $downloadedPackages = []; + foreach ($remoteEntriesToDownload as $package => $entry) { + if (!isset($contents[$package])) { + throw new \LogicException(sprintf('The package "%s" was not downloaded.', $package)); + } + + $filename = $this->savePackage($package, $contents[$package], $entry->type); + $newInstalled[$package] = [ + 'path' => $filename, + 'version' => $entry->version, + ]; + + $downloadedPackages[] = $package; + unset($contents[$package]); + } + + if ($contents) { + throw new \LogicException(sprintf('The following packages were unexpectedly downloaded: "%s".', implode('", "', array_keys($contents)))); + } + + $this->saveInstalled($newInstalled); + + 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 array + */ + private function loadInstalled(): array + { + if (isset($this->installed)) { + return $this->installed; + } + + $installedPath = $this->vendorDir.'/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; + } + + private function saveInstalled(array $installed): void + { + $this->installed = $installed; + file_put_contents($this->vendorDir.'/installed.php', sprintf('httpClient->request('GET', sprintf($this->versionUrlPattern, $packageName, urlencode($constraint))); $requiredPackages[] = [$options, $response, $packageName, $filePath, /* resolved version */ null]; @@ -103,16 +98,12 @@ public function resolvePackages(array $packagesToRequire): array continue; } - // final URL where it was redirected to - $url = $response->getInfo('url'); - $content = null; - - if ($options->download) { - $content = $this->parseJsDelivrImports($response->getContent(), $packagesToRequire, $options->download); - } - $packageName = trim($options->packageName, '/'); - $resolvedPackages[$packageName] = new ResolvedImportMapPackage($options, $url, $content); + $contentType = $response->getHeaders()['content-type'][0] ?? ''; + $type = str_starts_with($contentType, 'text/css') ? ImportMapType::CSS : ImportMapType::JS; + $resolvedPackages[$packageName] = new ResolvedImportMapPackage($options, $version, $type); + + $packagesToRequire = array_merge($packagesToRequire, $this->fetchPackageRequirementsFromImports($response->getContent())); } try { @@ -139,7 +130,7 @@ public function resolvePackages(array $packagesToRequire): array continue; } - $packagesToRequire[] = new PackageRequireOptions($packageName.$cssFile, $version, $options->download); + $packagesToRequire[] = new PackageRequireOptions($packageName.$cssFile, $version); } try { @@ -158,13 +149,50 @@ public function resolvePackages(array $packagesToRequire): array return array_values($resolvedPackages); } - public function getPackageVersion(string $url): ?string + /** + * @param ImportMapEntry[] $importMapEntries + * + * @return array + */ + public function downloadPackages(array $importMapEntries, callable $progressCallback = null): array { - if (1 === preg_match("#^https://cdn.jsdelivr.net/npm/(?(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*)@(?[\w\._-]+)(?/.*)?$#", $url, $matches)) { - return $matches['version']; + $responses = []; + + foreach ($importMapEntries as $package => $entry) { + [$packageName, $filePath] = self::splitPackageNameAndFilePath($entry->importName); + $pattern = ImportMapType::CSS === $entry->type ? $this->distUrlCssPattern : $this->distUrlPattern; + $url = sprintf($pattern, $packageName, $entry->version, $filePath); + + $responses[$package] = $this->httpClient->request('GET', $url); } - return null; + $errors = []; + $contents = []; + foreach ($responses as $package => $response) { + if (200 !== $response->getStatusCode()) { + $errors[] = [$package, $response]; + continue; + } + + if ($progressCallback) { + $progressCallback($package, 'started', $response, \count($responses)); + } + $contents[$package] = $this->makeImportsBare($response->getContent()); + if ($progressCallback) { + $progressCallback($package, 'finished', $response, \count($responses)); + } + } + + try { + ($errors[0][1] ?? null)?->getHeaders(); + } catch (HttpExceptionInterface $e) { + $response = $e->getResponse(); + $packages = implode('", "', array_column($errors, 0)); + + throw new RuntimeException(sprintf('Error %d downloading packages from jsDelivr for "%s". Check your package names. Response: ', $response->getStatusCode(), $packages).$response->getContent(false), 0, $e); + } + + return $contents; } /** @@ -173,18 +201,45 @@ public function getPackageVersion(string $url): ?string * Replaces those with normal import "package/name" statements and * records the package as a dependency, so it can be downloaded and * added to the importmap. + * + * @return PackageRequireOptions[] */ - private function parseJsDelivrImports(string $content, array &$dependencies, bool $download): string + private function fetchPackageRequirementsFromImports(string $content): array { // imports from jsdelivr follow a predictable format - $content = preg_replace_callback(self::IMPORT_REGEX, function ($matches) use (&$dependencies, $download) { - $packageName = $matches[1]; - $version = $matches[2]; + preg_match_all(self::IMPORT_REGEX, $content, $matches); + $dependencies = []; + foreach ($matches[1] as $index => $packageName) { + $version = $matches[2][$index]; - $dependencies[] = new PackageRequireOptions($packageName, $version, $download); + $dependencies[] = new PackageRequireOptions($packageName, $version); + } - return sprintf('from"%s"', $packageName); - }, $content); + return $dependencies; + } + + private 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]; + } + + /** + * Parses the very specific import syntax used by jsDelivr. + * + * Replaces those with normal import "package/name" statements. + */ + private function makeImportsBare(string $content): string + { + $content = preg_replace_callback(self::IMPORT_REGEX, fn ($m) => sprintf('from"%s"', $m[1]), $content); // source maps are not also downloaded - so remove the sourceMappingURL $content = preg_replace('{//# sourceMappingURL=.*$}m', '', $content); diff --git a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JspmResolver.php b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JspmResolver.php deleted file mode 100644 index 80e0c4d35bd4f..0000000000000 --- a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/JspmResolver.php +++ /dev/null @@ -1,108 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\AssetMapper\ImportMap\Resolver; - -use Symfony\Component\AssetMapper\Exception\RuntimeException; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; -use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; -use Symfony\Component\HttpClient\HttpClient; -use Symfony\Contracts\HttpClient\HttpClientInterface; - -final class JspmResolver implements PackageResolverInterface -{ - public const BASE_URI = 'https://api.jspm.io/'; - - private HttpClientInterface $httpClient; - - public function __construct( - HttpClientInterface $httpClient = null, - private readonly string $provider = ImportMapManager::PROVIDER_JSPM, - private readonly string $baseUri = self::BASE_URI, - ) { - $this->httpClient = $httpClient ?? HttpClient::create(); - } - - public function resolvePackages(array $packagesToRequire): array - { - if (!$packagesToRequire) { - return []; - } - - $installData = []; - $packageRequiresByName = []; - foreach ($packagesToRequire as $options) { - $constraint = $options->packageName; - if (null !== $options->versionConstraint) { - $constraint .= '@'.$options->versionConstraint; - } - if (null !== $options->registryName) { - $constraint = sprintf('%s:%s', $options->registryName, $constraint); - } - $installData[] = $constraint; - $packageRequiresByName[$options->packageName] = $options; - } - - $json = [ - 'install' => $installData, - 'flattenScope' => true, - // always grab production-ready assets - 'env' => ['browser', 'module', 'production'], - ]; - if (ImportMapManager::PROVIDER_JSPM !== $this->provider) { - $json['provider'] = $this->provider; - } - - $response = $this->httpClient->request('POST', 'generate', [ - 'base_uri' => $this->baseUri, - 'json' => $json, - ]); - - if (200 !== $response->getStatusCode()) { - $data = $response->toArray(false); - - if (isset($data['error'])) { - throw new RuntimeException('Error requiring JavaScript package: '.$data['error']); - } - - // Throws the original HttpClient exception - $response->getHeaders(); - } - - // if we're requiring just one package, in case it has any peer deps, match the download - $defaultOptions = $packagesToRequire[0]; - - $resolvedPackages = []; - foreach ($response->toArray()['map']['imports'] as $packageName => $url) { - $options = $packageRequiresByName[$packageName] ?? new PackageRequireOptions($packageName, null, $defaultOptions->download); - $resolvedPackages[] = [$options, $url, $options->download ? $this->httpClient->request('GET', $url, ['base_uri' => $this->baseUri]) : null]; - } - - try { - return array_map(fn ($args) => new ResolvedImportMapPackage($args[0], $args[1], $args[2]?->getContent()), $resolvedPackages); - } catch (\Throwable $e) { - foreach ($resolvedPackages as $args) { - $args[2]?->cancel(); - } - - throw $e; - } - } - - public function getPackageVersion(string $url): ?string - { - if (1 === preg_match("#^https://ga.jspm.io/npm:(?(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*)@(?[\w\._-]+)(?/.*)?$#", $url, $matches)) { - return $matches['version']; - } - - return null; - } -} diff --git a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolver.php b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolver.php deleted file mode 100644 index b2757c005e8dd..0000000000000 --- a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolver.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\AssetMapper\ImportMap\Resolver; - -use Psr\Container\ContainerInterface; - -final class PackageResolver implements PackageResolverInterface -{ - public function __construct( - private readonly string $provider, - private readonly ContainerInterface $locator, - ) { - } - - public function resolvePackages(array $packagesToRequire): array - { - return $this->locator->get($this->provider) - ->resolvePackages($packagesToRequire); - } - - public function getPackageVersion(string $url): ?string - { - return $this->locator->get($this->provider)->getPackageVersion($url); - } -} diff --git a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolverInterface.php b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolverInterface.php index 2613c13008d92..a569b06039d6a 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolverInterface.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/PackageResolverInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\AssetMapper\ImportMap\Resolver; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; interface PackageResolverInterface @@ -28,7 +29,13 @@ interface PackageResolverInterface public function resolvePackages(array $packagesToRequire): array; /** - * Tries to extract the package's version from its URL. + * Downloads the contents of the given packages. + * + * The returned array should be a map using the same keys as $importMapEntries. + * + * @param array $importMapEntries + * + * @return array */ - public function getPackageVersion(string $url): ?string; + public function downloadPackages(array $importMapEntries, callable $progressCallback = null): array; } diff --git a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/ResolvedImportMapPackage.php b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/ResolvedImportMapPackage.php index ed8a6cb854727..8c2c4e90e4bf2 100644 --- a/src/Symfony/Component/AssetMapper/ImportMap/Resolver/ResolvedImportMapPackage.php +++ b/src/Symfony/Component/AssetMapper/ImportMap/Resolver/ResolvedImportMapPackage.php @@ -11,15 +11,15 @@ namespace Symfony\Component\AssetMapper\ImportMap\Resolver; +use Symfony\Component\AssetMapper\ImportMap\ImportMapType; use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; final class ResolvedImportMapPackage { public function __construct( public readonly PackageRequireOptions $requireOptions, - public readonly string $url, - public readonly ?string $content = null, - public readonly ?string $version = null, + public readonly string $version, + public readonly ImportMapType $type, ) { } } diff --git a/src/Symfony/Component/AssetMapper/MappedAsset.php b/src/Symfony/Component/AssetMapper/MappedAsset.php index 0f0bef63fee74..58bfc93e52fe6 100644 --- a/src/Symfony/Component/AssetMapper/MappedAsset.php +++ b/src/Symfony/Component/AssetMapper/MappedAsset.php @@ -27,8 +27,11 @@ final class MappedAsset public readonly string $content; public readonly string $digest; public readonly bool $isPredigested; + public readonly bool $isVendor; /** + * Assets whose content affects the content of this asset. + * * @var MappedAsset[] */ private array $dependencies = []; @@ -55,6 +58,7 @@ public function __construct( string $content = null, string $digest = null, bool $isPredigested = null, + bool $isVendor = false, array $dependencies = [], array $fileDependencies = [], array $javaScriptImports = [], @@ -78,6 +82,7 @@ public function __construct( if (null !== $isPredigested) { $this->isPredigested = $isPredigested; } + $this->isVendor = $isVendor; $this->dependencies = $dependencies; $this->fileDependencies = $fileDependencies; $this->javaScriptImports = $javaScriptImports; diff --git a/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php b/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php index b99a904139c42..74642c012ee3e 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Command/AssetMapperCompileCommandTest.php @@ -69,7 +69,7 @@ public function testAssetsAreCompiled() $finder = new Finder(); $finder->in($targetBuildDir)->files(); - $this->assertCount(10, $finder); // 7 files + manifest.json & importmap.json + entrypoint.file6.json + $this->assertCount(12, $finder); // 9 files + manifest.json & importmap.json + entrypoint.file6.json $this->assertFileExists($targetBuildDir.'/manifest.json'); $this->assertSame([ @@ -78,6 +78,8 @@ public function testAssetsAreCompiled() 'file2.js', 'file3.css', 'file4.js', + 'lodash.js', + 'stimulus.js', 'subdir/file5.js', 'subdir/file6.js', ], array_keys(json_decode(file_get_contents($targetBuildDir.'/manifest.json'), true))); diff --git a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php index 3010548b961d7..e30c4361e93dc 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Compiler/JavaScriptImportPathCompilerTest.php @@ -40,7 +40,7 @@ public function testCompile(string $sourceLogicalName, string $input, array $exp } if ('module_in_importmap_remote' === $importName) { - return new ImportMapEntry('module_in_importmap_local_asset', url: 'https://example.com/module.js'); + return new ImportMapEntry('module_in_importmap_local_asset', version: '1.2.3'); } return null; diff --git a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php index c527b4678ba4c..c4b09bec5056a 100644 --- a/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php @@ -113,6 +113,14 @@ public function testCreateMappedAssetWithPredigested() $this->assertTrue($asset->isPredigested); } + public function testCreateMappedAssetInVendor() + { + $assetMapper = $this->createFactory(); + $asset = $assetMapper->createMappedAsset('lodash.js', __DIR__.'/../fixtures/assets/vendor/lodash.js'); + $this->assertSame('lodash.js', $asset->logicalPath); + $this->assertTrue($asset->isVendor); + } + private function createFactory(AssetCompilerInterface $extraCompiler = null): MappedAssetFactory { $compilers = [ @@ -137,7 +145,8 @@ private function createFactory(AssetCompilerInterface $extraCompiler = null): Ma $factory = new MappedAssetFactory( $pathResolver, - $compiler + $compiler, + __DIR__.'/../fixtures/assets/vendor', ); // mock the AssetMapper to behave like normal: by calling back to the factory diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php index fe8bc62624677..07e6512696dea 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapAuditorTest.php @@ -19,7 +19,6 @@ use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; use Symfony\Component\AssetMapper\ImportMap\ImportMapPackageAudit; use Symfony\Component\AssetMapper\ImportMap\ImportMapPackageAuditVulnerability; -use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -27,16 +26,14 @@ class ImportMapAuditorTest extends TestCase { private ImportMapConfigReader $importMapConfigReader; - private PackageResolverInterface $packageResolver; private HttpClientInterface $httpClient; private ImportMapAuditor $importMapAuditor; protected function setUp(): void { $this->importMapConfigReader = $this->createMock(ImportMapConfigReader::class); - $this->packageResolver = $this->createMock(PackageResolverInterface::class); $this->httpClient = new MockHttpClient(); - $this->importMapAuditor = new ImportMapAuditor($this->importMapConfigReader, $this->packageResolver, $this->httpClient); + $this->importMapAuditor = new ImportMapAuditor($this->importMapConfigReader, $this->httpClient); } public function testAudit() @@ -70,17 +67,14 @@ public function testAudit() $this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([ '@hotwired/stimulus' => new ImportMapEntry( importName: '@hotwired/stimulus', - url: 'https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js', version: '3.2.1', ), 'json5' => new ImportMapEntry( importName: 'json5', - url: 'https://cdn.jsdelivr.net/npm/json5@1.0.0/+esm', version: '1.0.0', ), 'lodash' => new ImportMapEntry( importName: 'lodash', - url: 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', version: '4.17.21', ), ])); @@ -126,7 +120,6 @@ public function testAuditWithVersionRange(bool $expectMatch, string $version, ?s $this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([ 'json5' => new ImportMapEntry( importName: 'json5', - url: "https://cdn.jsdelivr.net/npm/json5@$version/+esm", version: $version, ), ])); @@ -149,40 +142,12 @@ public function provideAuditWithVersionRange(): iterable yield [false, '1.2.0', '> 1.0.0, < 1.2.0']; } - public function testAuditWithVersionResolving() - { - $this->httpClient->setResponseFactory(new MockResponse('[]')); - $this->importMapConfigReader->method('getEntries')->willReturn(new ImportMapEntries([ - '@hotwired/stimulus' => new ImportMapEntry( - importName: '@hotwired/stimulus', - url: 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js', - version: '3.2.1', - ), - 'json5' => new ImportMapEntry( - importName: 'json5', - url: 'https://cdn.jsdelivr.net/npm/json5/+esm', - ), - 'lodash' => new ImportMapEntry( - importName: 'lodash', - url: 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', - ), - ])); - $this->packageResolver->method('getPackageVersion')->willReturn('1.2.3'); - - $audit = $this->importMapAuditor->audit(); - - $this->assertSame('3.2.1', $audit[0]->version); - $this->assertSame('1.2.3', $audit[1]->version); - $this->assertSame('1.2.3', $audit[2]->version); - } - 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', - url: 'https://cdn.jsdelivr.net/npm/json5@1.0.0/+esm', version: '1.0.0', ), ])); diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php index 0b971934e8606..da6636ae822c1 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php @@ -43,11 +43,7 @@ public function testGetEntriesAndWriteEntries() [ - 'url' => 'https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js', - ], - 'remote_package_downloaded' => [ - 'downloaded_to' => 'vendor/lodash.js', - 'url' => 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', + 'version' => '3.2.1', ], 'local_package' => [ 'path' => 'app.js', @@ -69,28 +65,23 @@ public function testGetEntriesAndWriteEntries() $this->assertInstanceOf(ImportMapEntries::class, $entries); /** @var ImportMapEntry[] $allEntries */ $allEntries = iterator_to_array($entries); - $this->assertCount(5, $allEntries); + $this->assertCount(4, $allEntries); $remotePackageEntry = $allEntries[0]; $this->assertSame('remote_package', $remotePackageEntry->importName); $this->assertNull($remotePackageEntry->path); - $this->assertSame('https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js', $remotePackageEntry->url); - $this->assertFalse($remotePackageEntry->isDownloaded); + $this->assertSame('3.2.1', $remotePackageEntry->version); $this->assertSame('js', $remotePackageEntry->type->value); $this->assertFalse($remotePackageEntry->isEntrypoint); - $remotePackageDownloadedEntry = $allEntries[1]; - $this->assertSame('https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', $remotePackageDownloadedEntry->url); - $this->assertSame('vendor/lodash.js', $remotePackageDownloadedEntry->path); - - $localPackageEntry = $allEntries[2]; - $this->assertNull($localPackageEntry->url); + $localPackageEntry = $allEntries[1]; + $this->assertNull($localPackageEntry->version); $this->assertSame('app.js', $localPackageEntry->path); - $typeCssEntry = $allEntries[3]; + $typeCssEntry = $allEntries[2]; $this->assertSame('css', $typeCssEntry->type->value); - $entryPointEntry = $allEntries[4]; + $entryPointEntry = $allEntries[3]; $this->assertTrue($entryPointEntry->isEntrypoint); // now save the original raw data from importmap.php and delete the file diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php index 2a7bcc519d2bc..51e4a25c60520 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php @@ -21,13 +21,12 @@ use Symfony\Component\AssetMapper\ImportMap\ImportMapType; use Symfony\Component\AssetMapper\ImportMap\JavaScriptImport; use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; +use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader; use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; use Symfony\Component\AssetMapper\ImportMap\Resolver\ResolvedImportMapPackage; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface; use Symfony\Component\Filesystem\Filesystem; -use Symfony\Contracts\HttpClient\HttpClientInterface; -use Symfony\Contracts\HttpClient\ResponseInterface; class ImportMapManagerTest extends TestCase { @@ -35,7 +34,7 @@ class ImportMapManagerTest extends TestCase private PublicAssetsPathResolverInterface&MockObject $pathResolver; private PackageResolverInterface&MockObject $packageResolver; private ImportMapConfigReader&MockObject $configReader; - private HttpClientInterface&MockObject $httpClient; + private RemotePackageDownloader&MockObject $remotePackageDownloader; private ImportMapManager $importMapManager; private Filesystem $filesystem; @@ -65,6 +64,7 @@ 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'); @@ -74,40 +74,23 @@ public function testGetRawImportMapData(array $importMapEntries, array $mappedAs public function getRawImportMapDataTests(): iterable { - yield 'it returns simple remote entry' => [ + yield 'it returns remote downloaded entry' => [ [ new ImportMapEntry( '@hotwired/stimulus', - url: 'https://anyurl.com/stimulus' - ), - ], - [], - [ - '@hotwired/stimulus' => [ - 'path' => 'https://anyurl.com/stimulus', - 'type' => 'js', - ], - ], - ]; - - yield 'it sets path to local path when remote package is downloaded' => [ - [ - new ImportMapEntry( - '@hotwired/stimulus', - path: 'vendor/stimulus.js', - url: 'https://anyurl.com/stimulus', - isDownloaded: true, + version: '1.2.3' ), ], [ new MappedAsset( - 'vendor/stimulus.js', - publicPath: '/assets/vendor/stimulus.js', + 'vendor/@hotwired/stimulus.js', + self::$writableRoot.'/assets/vendor/@hotwired/stimulus.js', + publicPath: '/assets/vendor/@hotwired/stimulus-d1g35t.js', ), ], [ '@hotwired/stimulus' => [ - 'path' => '/assets/vendor/stimulus.js', + 'path' => '/assets/vendor/@hotwired/stimulus-d1g35t.js', 'type' => 'js', ], ], @@ -644,23 +627,15 @@ public function testGetEntrypointNames() /** * @dataProvider getRequirePackageTests */ - public function testRequire(array $packages, int $expectedProviderPackageArgumentCount, array $resolvedPackages, array $expectedImportMap, array $expectedDownloadedFiles) + public function testRequire(array $packages, int $expectedProviderPackageArgumentCount, array $resolvedPackages, array $expectedImportMap) { $manager = $this->createImportMapManager(); // physical file we point to in one test $this->writeFile('assets/some_file.js', 'some file contents'); - // make it so that downloaded files are found in AssetMapper $this->assetMapper->expects($this->any()) ->method('getAssetFromSourcePath') - ->willReturnCallback(function (string $sourcePath) use ($expectedDownloadedFiles) { - foreach ($expectedDownloadedFiles as $file => $contents) { - $expectedPath = self::$writableRoot.'/assets/vendor/'.$file; - if (realpath($expectedPath) === realpath($sourcePath)) { - return new MappedAsset('vendor/'.$file, $sourcePath); - } - } - + ->willReturnCallback(function (string $sourcePath) { if (str_ends_with($sourcePath, 'some_file.js')) { // physical file we point to in one test return new MappedAsset('some_file.js', $sourcePath); @@ -685,8 +660,8 @@ public function testRequire(array $packages, int $expectedProviderPackageArgumen $simplifiedEntries = []; foreach ($entries as $entry) { $simplifiedEntries[$entry->importName] = [ - 'url' => $entry->url, - ($entry->isDownloaded ? 'downloaded_to' : 'path') => $entry->path, + 'version' => $entry->version, + 'path' => $entry->path, 'type' => $entry->type->value, 'entrypoint' => $entry->isEntrypoint, ]; @@ -695,7 +670,9 @@ public function testRequire(array $packages, int $expectedProviderPackageArgumen $this->assertSame(array_keys($expectedImportMap), array_keys($simplifiedEntries)); foreach ($expectedImportMap as $name => $expectedData) { foreach ($expectedData as $key => $val) { - $this->assertSame($val, $simplifiedEntries[$name][$key]); + // correct windows paths for comparison + $actualPath = str_replace('\\', '/', $simplifiedEntries[$name][$key]); + $this->assertSame($val, $actualPath); } } @@ -712,11 +689,6 @@ public function testRequire(array $packages, int $expectedProviderPackageArgumen ; $manager->require($packages); - foreach ($expectedDownloadedFiles as $file => $expectedContents) { - $this->assertFileExists(self::$writableRoot.'/assets/vendor/'.$file); - $actualContents = file_get_contents(self::$writableRoot.'/assets/vendor/'.$file); - $this->assertSame($expectedContents, $actualContents); - } } public static function getRequirePackageTests(): iterable @@ -725,113 +697,60 @@ public static function getRequirePackageTests(): iterable 'packages' => [new PackageRequireOptions('lodash')], 'expectedProviderPackageArgumentCount' => 1, 'resolvedPackages' => [ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js'), + self::resolvedPackage('lodash', '1.2.3'), ], 'expectedImportMap' => [ 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', + 'version' => '1.2.3', ], ], - 'expectedDownloadedFiles' => [], ]; yield 'require two packages' => [ 'packages' => [new PackageRequireOptions('lodash'), new PackageRequireOptions('cowsay')], 'expectedProviderPackageArgumentCount' => 2, 'resolvedPackages' => [ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js'), - self::resolvedPackage('cowsay', 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.js'), + self::resolvedPackage('lodash', '1.2.3'), + self::resolvedPackage('cowsay', '4.5.6'), ], 'expectedImportMap' => [ 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', + 'version' => '1.2.3', ], 'cowsay' => [ - 'url' => 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.js', + 'version' => '4.5.6', ], ], - 'expectedDownloadedFiles' => [], ]; yield 'single_package_that_returns_as_two' => [ 'packages' => [new PackageRequireOptions('lodash')], 'expectedProviderPackageArgumentCount' => 1, 'resolvedPackages' => [ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js'), - self::resolvedPackage('lodash-dependency', 'https://ga.jspm.io/npm:lodash-dependency@9.8.7/lodash-dependency.js'), + self::resolvedPackage('lodash', '1.2.3'), + self::resolvedPackage('lodash-dependency', '9.8.7'), ], 'expectedImportMap' => [ 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', + 'version' => '1.2.3', ], 'lodash-dependency' => [ - 'url' => 'https://ga.jspm.io/npm:lodash-dependency@9.8.7/lodash-dependency.js', + 'version' => '9.8.7', ], ], - 'expectedDownloadedFiles' => [], ]; yield 'single_package_with_version_constraint' => [ 'packages' => [new PackageRequireOptions('lodash', '^1.2.3')], 'expectedProviderPackageArgumentCount' => 1, 'resolvedPackages' => [ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.7/lodash.js'), + self::resolvedPackage('lodash', '1.2.7'), ], 'expectedImportMap' => [ 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.7/lodash.js', - ], - ], - 'expectedDownloadedFiles' => [], - ]; - - yield 'single_package_that_downloads' => [ - 'packages' => [new PackageRequireOptions('lodash', download: true)], - 'expectedProviderPackageArgumentCount' => 1, - 'resolvedPackages' => [ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', download: true, content: 'the code in lodash.js'), - ], - 'expectedImportMap' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - 'downloaded_to' => 'vendor/lodash.js', - ], - ], - 'expectedDownloadedFiles' => [ - 'lodash.js' => 'the code in lodash.js', - ], - ]; - - yield 'single_package_that_downloads_a_css_file' => [ - 'packages' => [new PackageRequireOptions('bootstrap/dist/css/bootstrap.min.css', download: true)], - 'expectedProviderPackageArgumentCount' => 1, - 'resolvedPackages' => [ - self::resolvedPackage('bootstrap/dist/css/bootstrap.min.css', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css', download: true, content: 'some sweet CSS'), - ], - 'expectedImportMap' => [ - 'bootstrap/dist/css/bootstrap.min.css' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css', - 'downloaded_to' => 'vendor/bootstrap/dist/css/bootstrap.min.css', - 'type' => 'css', - ], - ], - 'expectedDownloadedFiles' => [ - 'bootstrap/dist/css/bootstrap.min.css' => 'some sweet CSS', - ], - ]; - - yield 'single_package_with_custom_import_name' => [ - 'packages' => [new PackageRequireOptions('lodash', importName: 'lodash-es')], - 'expectedProviderPackageArgumentCount' => 1, - 'resolvedPackages' => [ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', importName: 'lodash-es'), - ], - 'expectedImportMap' => [ - 'lodash-es' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', + 'version' => '1.2.7', ], ], - 'expectedDownloadedFiles' => [], ]; yield 'single_package_with_a_path' => [ @@ -844,7 +763,6 @@ public static function getRequirePackageTests(): iterable 'path' => './assets/some_file.js', ], ], - 'expectedDownloadedFiles' => [], ]; } @@ -852,9 +770,9 @@ public function testRemove() { $manager = $this->createImportMapManager(); $this->mockImportMap([ - new ImportMapEntry('lodash', url: 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js'), - new ImportMapEntry('cowsay', path: 'vendor/moo.js', url: 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.umd.js', isDownloaded: true), - new ImportMapEntry('chance', path: 'vendor/chance.js', url: 'https://ga.jspm.io/npm:chance@7.8.9/build/chance.js', isDownloaded: true), + 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'), ]); @@ -864,12 +782,6 @@ public function testRemove() new MappedAsset('app.js', self::$writableRoot.'/assets/app.js'), ]); - $this->filesystem->mkdir(self::$writableRoot.'/assets/vendor'); - touch(self::$writableRoot.'/assets/vendor/moo.js'); - touch(self::$writableRoot.'/assets/vendor/chance.js'); - touch(self::$writableRoot.'/assets/app.js'); - touch(self::$writableRoot.'/assets/other.js'); - $this->configReader->expects($this->once()) ->method('writeEntries') ->with($this->callback(function (ImportMapEntries $entries) { @@ -883,107 +795,73 @@ public function testRemove() ; $manager->remove(['cowsay', 'app']); - $this->assertFileDoesNotExist(self::$writableRoot.'/assets/vendor/moo.js'); - $this->assertFileDoesNotExist(self::$writableRoot.'/assets/app.js'); - $this->assertFileExists(self::$writableRoot.'/assets/vendor/chance.js'); - $this->assertFileExists(self::$writableRoot.'/assets/other.js'); } public function testUpdateAll() { $manager = $this->createImportMapManager(); $this->mockImportMap([ - new ImportMapEntry('lodash', url: 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js'), - new ImportMapEntry('cowsay', path: 'vendor/moo.js', url: 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.umd.js', isDownloaded: true), - new ImportMapEntry('bootstrap', url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.esm.js'), + new ImportMapEntry('lodash', version: '1.2.3'), + new ImportMapEntry('bootstrap', version: '5.1.3'), new ImportMapEntry('app', path: 'app.js'), ]); - $this->mockAssetMapper([ - new MappedAsset('vendor/moo.js', self::$writableRoot.'/assets/vendor/moo.js'), - ], false); - $this->assetMapper->expects($this->any()) - ->method('getAssetFromSourcePath') - ->willReturnCallback(function (string $sourcePath) { - if (str_ends_with($sourcePath, 'assets/vendor/cowsay.js')) { - return new MappedAsset('vendor/cowsay.js'); - } - - return null; - }) - ; - - $this->filesystem->mkdir(self::$writableRoot.'/assets/vendor'); - file_put_contents(self::$writableRoot.'/assets/vendor/moo.js', 'moo.js contents'); - file_put_contents(self::$writableRoot.'/assets/app.js', 'app.js contents'); - $this->packageResolver->expects($this->once()) ->method('resolvePackages') ->with($this->callback(function ($packages) { $this->assertInstanceOf(PackageRequireOptions::class, $packages[0]); /* @var PackageRequireOptions[] $packages */ - $this->assertCount(3, $packages); + $this->assertCount(2, $packages); $this->assertSame('lodash', $packages[0]->packageName); - $this->assertFalse($packages[0]->download); - - $this->assertSame('cowsay', $packages[1]->packageName); - $this->assertTrue($packages[1]->download); - - $this->assertSame('bootstrap', $packages[2]->packageName); + $this->assertSame('bootstrap', $packages[1]->packageName); return true; })) ->willReturn([ - self::resolvedPackage('lodash', 'https://ga.jspm.io/npm:lodash@1.2.9/lodash.js'), - self::resolvedPackage('cowsay', 'https://ga.jspm.io/npm:cowsay@4.5.9/cowsay.umd.js', download: true, content: 'contents of cowsay.js'), - self::resolvedPackage('bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.esm.js'), + self::resolvedPackage('lodash', '1.2.9'), + self::resolvedPackage('bootstrap', '5.2.3'), ]) ; $this->configReader->expects($this->once()) ->method('writeEntries') ->with($this->callback(function (ImportMapEntries $entries) { - $this->assertCount(4, $entries); + $this->assertCount(3, $entries); $this->assertTrue($entries->has('lodash')); - $this->assertTrue($entries->has('cowsay')); $this->assertTrue($entries->has('bootstrap')); $this->assertTrue($entries->has('app')); - $this->assertSame('https://ga.jspm.io/npm:lodash@1.2.9/lodash.js', $entries->get('lodash')->url); - $this->assertSame('https://ga.jspm.io/npm:cowsay@4.5.9/cowsay.umd.js', $entries->get('cowsay')->url); - $this->assertSame('https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.esm.js', $entries->get('bootstrap')->url); + $this->assertSame('1.2.9', $entries->get('lodash')->version); + $this->assertSame('5.2.3', $entries->get('bootstrap')->version); return true; })) ; $manager->update(); - $this->assertFileDoesNotExist(self::$writableRoot.'/assets/vendor/moo.js'); - $this->assertFileExists(self::$writableRoot.'/assets/vendor/cowsay.js'); - $actualContents = file_get_contents(self::$writableRoot.'/assets/vendor/cowsay.js'); - $this->assertSame('contents of cowsay.js', $actualContents); } public function testUpdateWithSpecificPackages() { $manager = $this->createImportMapManager(); $this->mockImportMap([ - new ImportMapEntry('lodash', url: 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js'), - new ImportMapEntry('cowsay', path: 'vendor/cowsay.js', url: 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.umd.js', isDownloaded: true), - new ImportMapEntry('bootstrap', url: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.esm.js'), + 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'), ]); - $this->writeFile('assets/vendor/cowsay.js', 'cowsay.js original contents'); - $this->packageResolver->expects($this->once()) ->method('resolvePackages') ->willReturn([ - self::resolvedPackage('cowsay', 'https://ga.jspm.io/npm:cowsay@4.5.9/cowsay.umd.js', download: true, content: 'updated contents of cowsay.js'), + self::resolvedPackage('cowsay', '4.5.9'), ]) ; + $this->remotePackageDownloader->expects($this->once()) + ->method('downloadPackages'); + $this->configReader->expects($this->any()) ->method('getRootDirectory') ->willReturn(self::$writableRoot); @@ -992,61 +870,14 @@ public function testUpdateWithSpecificPackages() ->with($this->callback(function (ImportMapEntries $entries) { $this->assertCount(4, $entries); - $this->assertSame('https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', $entries->get('lodash')->url); - $this->assertSame('https://ga.jspm.io/npm:cowsay@4.5.9/cowsay.umd.js', $entries->get('cowsay')->url); + $this->assertSame('1.2.3', $entries->get('lodash')->version); + $this->assertSame('4.5.9', $entries->get('cowsay')->version); return true; })) ; - $this->mockAssetMapper([ - new MappedAsset('vendor/cowsay.js', self::$writableRoot.'/assets/vendor/cowsay.js'), - ]); - $manager->update(['cowsay']); - $actualContents = file_get_contents(self::$writableRoot.'/assets/vendor/cowsay.js'); - $this->assertSame('updated contents of cowsay.js', $actualContents); - } - - public function testDownloadMissingPackages() - { - $manager = $this->createImportMapManager(); - $this->mockImportMap([ - new ImportMapEntry('@hotwired/stimulus', path: 'vendor/@hotwired/stimulus.js', url: 'https://cdn.jsdelivr.net/npm/stimulus@3.2.1/+esm', isDownloaded: true), - new ImportMapEntry('lodash', path: 'vendor/lodash.js', url: 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', isDownloaded: true), - ]); - - $this->mockAssetMapper([ - // fake that vendor/lodash.js exists, but not stimulus - new MappedAsset('vendor/lodash.js'), - ], false); - $this->assetMapper->expects($this->any()) - ->method('getAssetFromSourcePath') - ->willReturnCallback(function (string $sourcePath) { - if (str_ends_with($sourcePath, 'assets/vendor/@hotwired/stimulus.js')) { - return new MappedAsset('vendor/@hotwired/stimulus.js'); - } - }) - ; - - $response = $this->createMock(ResponseInterface::class); - $response->expects($this->once()) - ->method('getContent') - ->willReturn('contents of stimulus.js'); - - $this->httpClient->expects($this->once()) - ->method('request') - ->willReturn($response); - - $downloadedPackages = $manager->downloadMissingPackages(); - $this->assertCount(1, $downloadedPackages); - - $expectedDownloadedFiles = [ - '' => 'contents of stimulus.js', - ]; - $downloadPath = self::$writableRoot.'/assets/vendor/@hotwired/stimulus.js'; - $this->assertFileExists($downloadPath); - $this->assertSame('contents of stimulus.js', file_get_contents($downloadPath)); } /** @@ -1074,7 +905,6 @@ public static function getPackageNameTests(): iterable 'lodash', [ 'package' => 'lodash', - 'registry' => '', ], ]; @@ -1082,24 +912,6 @@ public static function getPackageNameTests(): iterable 'lodash@^1.2.3', [ 'package' => 'lodash', - 'registry' => '', - 'version' => '^1.2.3', - ], - ]; - - yield 'with_registry' => [ - 'npm:lodash', - [ - 'package' => 'lodash', - 'registry' => 'npm', - ], - ]; - - yield 'with_registry_and_version' => [ - 'npm:lodash@^1.2.3', - [ - 'package' => 'lodash', - 'registry' => 'npm', 'version' => '^1.2.3', ], ]; @@ -1108,7 +920,6 @@ public static function getPackageNameTests(): iterable '@hotwired/stimulus', [ 'package' => '@hotwired/stimulus', - 'registry' => '', ], ]; @@ -1116,24 +927,6 @@ public static function getPackageNameTests(): iterable '@hotwired/stimulus@^1.2.3', [ 'package' => '@hotwired/stimulus', - 'registry' => '', - 'version' => '^1.2.3', - ], - ]; - - yield 'namespaced_package_with_registry_no_version' => [ - 'npm:@hotwired/stimulus', - [ - 'package' => '@hotwired/stimulus', - 'registry' => 'npm', - ], - ]; - - yield 'namespaced_package_with_registry_and_version' => [ - 'npm:@hotwired/stimulus@^1.2.3', - [ - 'package' => '@hotwired/stimulus', - 'registry' => 'npm', 'version' => '^1.2.3', ], ]; @@ -1145,24 +938,23 @@ private function createImportMapManager(): ImportMapManager $this->assetMapper = $this->createMock(AssetMapperInterface::class); $this->configReader = $this->createMock(ImportMapConfigReader::class); $this->packageResolver = $this->createMock(PackageResolverInterface::class); - $this->httpClient = $this->createMock(HttpClientInterface::class); + $this->remotePackageDownloader = $this->createMock(RemotePackageDownloader::class); return $this->importMapManager = new ImportMapManager( $this->assetMapper, $this->pathResolver, $this->configReader, - self::$writableRoot.'/assets/vendor', + $this->remotePackageDownloader, $this->packageResolver, - $this->httpClient, ); } - private static function resolvedPackage(string $packageName, string $url, bool $download = false, string $importName = null, string $content = null) + private static function resolvedPackage(string $packageName, string $version, ImportMapType $type = ImportMapType::JS) { return new ResolvedImportMapPackage( - new PackageRequireOptions($packageName, download: $download, importName: $importName), - $url, - $content, + new PackageRequireOptions($packageName), + $version, + $type, ); } @@ -1231,6 +1023,25 @@ 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 $packageName) use ($importMapEntries) { + foreach ($importMapEntries as $entry) { + if ($entry->importName === $packageName) { + return self::$writableRoot.'/assets/vendor/'.$packageName.'.js'; + } + } + + return null; + }) + ; + } + private function writeFile(string $filename, string $content): void { $path = \dirname(self::$writableRoot.'/'.$filename); diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php new file mode 100644 index 0000000000000..2aaee06c01793 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/RemotePackageDownloaderTest.php @@ -0,0 +1,177 @@ + + * + * 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\ImportMapConfigReader; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntries; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; +use Symfony\Component\AssetMapper\ImportMap\ImportMapType; +use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader; +use Symfony\Component\AssetMapper\ImportMap\Resolver\PackageResolverInterface; +use Symfony\Component\Filesystem\Filesystem; + +class RemotePackageDownloaderTest 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 testDownloadPackagesDownloadsEverythingWithNoInstalled() + { + $configReader = $this->createMock(ImportMapConfigReader::class); + $packageResolver = $this->createMock(PackageResolverInterface::class); + + $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]); + + $configReader->expects($this->once()) + ->method('getEntries') + ->willReturn($importMapEntries); + + $progressCallback = fn () => null; + $packageResolver->expects($this->once()) + ->method('downloadPackages') + ->with( + ['foo' => $entry1, 'bar.js/file' => $entry2, 'baz' => $entry3], + $progressCallback + ) + ->willReturn(['foo' => 'foo content', 'bar.js/file' => 'bar content', 'baz' => 'baz content']); + + $downloader = new RemotePackageDownloader( + $configReader, + $packageResolver, + self::$writableRoot.'/assets/vendor', + ); + $downloader->downloadPackages($progressCallback); + + $this->assertFileExists(self::$writableRoot.'/assets/vendor/foo.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->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')); + + $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'], + ], + $installed + ); + } + + 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'], + ]; + file_put_contents( + self::$writableRoot.'/assets/vendor/installed.php', + 'createMock(ImportMapConfigReader::class); + $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'); + // matches installed version but file does not exist + $entry2 = new ImportMapEntry('bar.js/file', version: '1.0.0'); + // 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'); + $importMapEntries = new ImportMapEntries([$entry1, $entry2, $entry3]); + + $configReader->expects($this->once()) + ->method('getEntries') + ->willReturn($importMapEntries); + + $packageResolver->expects($this->once()) + ->method('downloadPackages') + ->willReturn(['bar.js/file' => 'new bar content', 'baz' => 'new baz content']); + + $downloader = new RemotePackageDownloader( + $configReader, + $packageResolver, + self::$writableRoot.'/assets/vendor', + ); + $downloader->downloadPackages(); + + $this->assertFileExists(self::$writableRoot.'/assets/vendor/foo.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->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')); + + $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'], + ], + $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() + { + $downloader = new RemotePackageDownloader( + $this->createMock(ImportMapConfigReader::class), + $this->createMock(PackageResolverInterface::class), + self::$writableRoot.'/assets/vendor', + ); + $this->assertSame(realpath(self::$writableRoot.'/assets/vendor'), realpath($downloader->getVendorDir())); + } +} diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php index 220107953c0b3..2c667787e4e7b 100644 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php +++ b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JsDelivrEsmResolverTest.php @@ -12,6 +12,8 @@ namespace Symfony\Component\AssetMapper\Tests\ImportMap\Resolver; use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; +use Symfony\Component\AssetMapper\ImportMap\ImportMapType; use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver; use Symfony\Component\HttpClient\MockHttpClient; @@ -35,9 +37,7 @@ public function testResolvePackages(array $packages, array $expectedRequests, ar $body = \is_array($expectedRequest['response']['body']) ? json_encode($expectedRequest['response']['body']) : $expectedRequest['response']['body']; } - return new MockResponse($body, [ - 'url' => $expectedRequest['response']['url'] ?? '/anything', - ]); + return new MockResponse($body); }; } @@ -49,11 +49,10 @@ public function testResolvePackages(array $packages, array $expectedRequests, ar foreach ($actualResolvedPackages as $package) { $packageName = $package->requireOptions->packageName; $this->assertArrayHasKey($packageName, $expectedResolvedPackages); - $this->assertSame($expectedResolvedPackages[$packageName]['url'], $package->url); - if (isset($expectedResolvedPackages[$packageName]['content'])) { - $this->assertSame($expectedResolvedPackages[$packageName]['content'], $package->content); - } + $this->assertSame($expectedResolvedPackages[$packageName]['version'], $package->version); } + + $this->assertSame(\count($expectedRequests), $httpClient->getRequestsCount()); } public static function provideResolvePackagesTests(): iterable @@ -67,7 +66,6 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/lodash@1.2.3/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm'], ], [ 'url' => '/v1/packages/npm/lodash@1.2.3/entrypoints', @@ -76,7 +74,7 @@ public static function provideResolvePackagesTests(): iterable ], 'expectedResolvedPackages' => [ 'lodash' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm', + 'version' => '1.2.3', ], ], ]; @@ -90,7 +88,6 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/lodash@2.1.3/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/lodash.js@2.1.3/+esm'], ], [ 'url' => '/v1/packages/npm/lodash@2.1.3/entrypoints', @@ -99,7 +96,7 @@ public static function provideResolvePackagesTests(): iterable ], 'expectedResolvedPackages' => [ 'lodash' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/lodash.js@2.1.3/+esm', + 'version' => '2.1.3', ], ], ]; @@ -113,7 +110,6 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/@hotwired/stimulus@3.1.3/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/@hotwired/stimulus.js@3.1.3/+esm'], ], [ 'url' => '/v1/packages/npm/@hotwired/stimulus@3.1.3/entrypoints', @@ -122,7 +118,7 @@ public static function provideResolvePackagesTests(): iterable ], 'expectedResolvedPackages' => [ '@hotwired/stimulus' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@hotwired/stimulus.js@3.1.3/+esm', + 'version' => '3.1.3', ], ], ]; @@ -136,16 +132,11 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/chart.js@3.0.1/auto/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/chart.js@3.0.1/auto/+esm'], - ], - [ - 'url' => '/v1/packages/npm/chart.js@3.0.1/entrypoints', - 'response' => ['body' => ['entrypoints' => []]], ], ], 'expectedResolvedPackages' => [ 'chart.js/auto' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/chart.js@3.0.1/auto/+esm', + 'version' => '3.0.1', ], ], ]; @@ -159,113 +150,44 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/@chart/chart.js@3.0.1/auto/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/@chart/chart.js@3.0.1/auto/+esm'], - ], - [ - 'url' => '/v1/packages/npm/@chart/chart.js@3.0.1/entrypoints', - 'response' => ['body' => ['entrypoints' => []]], ], ], 'expectedResolvedPackages' => [ '@chart/chart.js/auto' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@chart/chart.js@3.0.1/auto/+esm', - ], - ], - ]; - - yield 'require package with simple download' => [ - 'packages' => [new PackageRequireOptions('lodash', download: true)], - 'expectedRequests' => [ - [ - 'url' => '/v1/packages/npm/lodash/resolved?specifier=%2A', - 'response' => ['body' => ['version' => '1.2.3']], - ], - [ - 'url' => '/lodash@1.2.3/+esm', - 'response' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm', - 'body' => 'contents of file', - ], - ], - [ - 'url' => '/v1/packages/npm/lodash@1.2.3/entrypoints', - 'response' => ['body' => ['entrypoints' => []]], - ], - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm', - 'content' => 'contents of file', + 'version' => '3.0.1', ], ], ]; - yield 'require package download with import dependencies' => [ - 'packages' => [new PackageRequireOptions('lodash', download: true)], + yield 'require package that imports another' => [ + 'packages' => [new PackageRequireOptions('@chart/chart.js/auto', '^3')], 'expectedRequests' => [ - // lodash [ - 'url' => '/v1/packages/npm/lodash/resolved?specifier=%2A', - 'response' => ['body' => ['version' => '1.2.3']], - ], - [ - 'url' => '/lodash@1.2.3/+esm', - 'response' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm', - 'body' => 'import{Color as t}from"/npm/@kurkle/color@0.3.2/+esm";console.log("yo");', - ], + 'url' => '/v1/packages/npm/@chart/chart.js/resolved?specifier=%5E3', + 'response' => ['body' => ['version' => '3.0.1']], ], [ - 'url' => '/v1/packages/npm/lodash@1.2.3/entrypoints', - 'response' => ['body' => ['entrypoints' => []]], + 'url' => '/@chart/chart.js@3.0.1/auto/+esm', + 'response' => ['body' => 'import{Color as t}from"/npm/@kurkle/color@0.3.2/+esm";function e(){}const i=(()='], ], - // @kurkle/color [ 'url' => '/v1/packages/npm/@kurkle/color/resolved?specifier=0.3.2', 'response' => ['body' => ['version' => '0.3.2']], ], [ 'url' => '/@kurkle/color@0.3.2/+esm', - 'response' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@kurkle/color@0.3.2/+esm', - 'body' => 'import*as t from"/npm/@popperjs/core@2.11.7/+esm";// hello world', - ], ], [ 'url' => '/v1/packages/npm/@kurkle/color@0.3.2/entrypoints', 'response' => ['body' => ['entrypoints' => []]], ], - // @popperjs/core - [ - 'url' => '/v1/packages/npm/@popperjs/core/resolved?specifier=2.11.7', - 'response' => ['body' => ['version' => '2.11.7']], - ], - [ - 'url' => '/@popperjs/core@2.11.7/+esm', - 'response' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/+esm', - // point back to the original to try to confuse things or cause extra work - 'body' => 'import*as t from"/npm/lodash@1.2.9/+esm";// hello from popper', - ], - ], - [ - 'url' => '/v1/packages/npm/@popperjs/core@2.11.7/entrypoints', - 'response' => ['body' => ['entrypoints' => []]], - ], ], 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm', - // file was updated correctly - 'content' => 'import{Color as t}from"@kurkle/color";console.log("yo");', + '@chart/chart.js/auto' => [ + 'version' => '3.0.1', ], '@kurkle/color' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@kurkle/color@0.3.2/+esm', - 'content' => 'import*as t from"@popperjs/core";// hello world', - ], - '@popperjs/core' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/+esm', - 'content' => 'import*as t from"lodash";// hello from popper', + 'version' => '0.3.2', ], ], ]; @@ -280,12 +202,11 @@ public static function provideResolvePackagesTests(): iterable [ // CSS is detected: +esm is left off 'url' => '/bootstrap@3.3.0/dist/css/bootstrap.min.css', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@3.3.0/dist/css/bootstrap.min.css'], ], ], 'expectedResolvedPackages' => [ 'bootstrap/dist/css/bootstrap.min.css' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@3.3.0/dist/css/bootstrap.min.css', + 'version' => '3.3.0', ], ], ]; @@ -299,7 +220,6 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/bootstrap@5.2.0/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@5.2.0/+esm'], ], [ 'url' => '/v1/packages/npm/bootstrap@5.2.0/entrypoints', @@ -314,15 +234,14 @@ public static function provideResolvePackagesTests(): iterable [ // grab the found CSS 'url' => '/bootstrap@5.2.0/dist/css/bootstrap.min.css', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@5.2.0/dist/css/bootstrap.min.css'], ], ], 'expectedResolvedPackages' => [ 'bootstrap' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@5.2.0/+esm', + 'version' => '5.2.0', ], 'bootstrap/dist/css/bootstrap.min.css' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@5.2.0/dist/css/bootstrap.min.css', + 'version' => '5.2.0', ], ], ]; @@ -336,17 +255,155 @@ public static function provideResolvePackagesTests(): iterable ], [ 'url' => '/bootstrap@5.2.0/dist/modal.js/+esm', - 'response' => ['url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@5.2.0/dist/modal.js+esm'], ], ], 'expectedResolvedPackages' => [ 'bootstrap/dist/modal.js' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/bootstrap.js@5.2.0/dist/modal.js+esm', + 'version' => '5.2.0', ], ], ]; } + /** + * @dataProvider provideDownloadPackagesTests + */ + public function testDownloadPackages(array $importMapEntries, array $expectedRequests, array $expectedContents) + { + $responses = []; + foreach ($expectedRequests as $expectedRequest) { + $responses[] = function ($method, $url) use ($expectedRequest) { + $this->assertSame('GET', $method); + $this->assertStringEndsWith($expectedRequest['url'], $url); + + return new MockResponse($expectedRequest['body']); + }; + } + + $httpClient = new MockHttpClient($responses); + + $provider = new JsDelivrEsmResolver($httpClient); + $actualContents = $provider->downloadPackages($importMapEntries); + $this->assertCount(\count($expectedContents), $actualContents); + $actualContents = array_map('trim', $actualContents); + $this->assertSame($expectedContents, $actualContents); + $this->assertSame(\count($expectedRequests), $httpClient->getRequestsCount()); + } + + public static function provideDownloadPackagesTests() + { + yield 'single package' => [ + ['lodash' => new ImportMapEntry('lodash', version: '1.2.3')], + [ + [ + 'url' => '/lodash@1.2.3/+esm', + 'body' => 'lodash contents', + ], + ], + [ + 'lodash' => 'lodash contents', + ], + ]; + + yield 'package with path' => [ + ['lodash' => new ImportMapEntry('chart.js/auto', version: '4.5.6')], + [ + [ + 'url' => '/chart.js@4.5.6/auto/+esm', + 'body' => 'chart.js contents', + ], + ], + [ + 'lodash' => 'chart.js contents', + ], + ]; + + yield 'css file' => [ + ['lodash' => new ImportMapEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS)], + [ + [ + 'url' => '/bootstrap@5.0.6/dist/bootstrap.css', + 'body' => 'bootstrap.css contents', + ], + ], + [ + 'lodash' => 'bootstrap.css contents', + ], + ]; + + yield 'multiple files' => [ + [ + 'lodash' => new ImportMapEntry('lodash', version: '1.2.3'), + 'chart.js/auto' => new ImportMapEntry('chart.js/auto', version: '4.5.6'), + 'bootstrap/dist/bootstrap.css' => new ImportMapEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS), + ], + [ + [ + 'url' => '/lodash@1.2.3/+esm', + 'body' => 'lodash contents', + ], + [ + 'url' => '/chart.js@4.5.6/auto/+esm', + 'body' => 'chart.js contents', + ], + [ + 'url' => '/bootstrap@5.0.6/dist/bootstrap.css', + 'body' => 'bootstrap.css contents', + ], + ], + [ + 'lodash' => 'lodash contents', + 'chart.js/auto' => 'chart.js contents', + 'bootstrap/dist/bootstrap.css' => 'bootstrap.css contents', + ], + ]; + + yield 'make imports relative' => [ + [ + '@chart.js/auto' => new ImportMapEntry('chart.js/auto', version: '1.2.3'), + ], + [ + [ + 'url' => '/chart.js@1.2.3/auto/+esm', + 'body' => 'import{Color as t}from"/npm/@kurkle/color@0.3.2/+esm";function e(){}const i=(()=', + ], + ], + [ + '@chart.js/auto' => 'import{Color as t}from"@kurkle/color";function e(){}const i=(()=', + ], + ]; + + yield 'js importmap is removed' => [ + [ + '@chart.js/auto' => new ImportMapEntry('chart.js/auto', version: '1.2.3'), + ], + [ + [ + 'url' => '/chart.js@1.2.3/auto/+esm', + 'body' => 'as Ticks,ta as TimeScale,ia as TimeSeriesScale,oo as Title,wo as Tooltip,Ci as _adapters,us as _detectPlatform,Ye as animator,Si as controllers,tn as default,St as defaults,Pn as elements,qi as layouts,ko as plugins,na as registerables,Ps as registry,sa as scales}; + //# sourceMappingURL=/sm/bc823a081dbde2b3a5424732858022f831d3f2978d59498cd938e0c2c8cf9ec0.map', + ], + ], + [ + '@chart.js/auto' => 'as Ticks,ta as TimeScale,ia as TimeSeriesScale,oo as Title,wo as Tooltip,Ci as _adapters,us as _detectPlatform,Ye as animator,Si as controllers,tn as default,St as defaults,Pn as elements,qi as layouts,ko as plugins,na as registerables,Ps as registry,sa as scales};', + ], + ]; + + yield 'css file removes importmap' => [ + ['lodash' => new ImportMapEntry('bootstrap/dist/bootstrap.css', version: '5.0.6', type: ImportMapType::CSS)], + [ + [ + 'url' => '/bootstrap@5.0.6/dist/bootstrap.css', + 'body' => 'print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} + /*# sourceMappingURL=bootstrap.min.css.map */', + ], + ], + [ + 'lodash' => 'print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}', + ], + ]; + } + /** * @dataProvider provideImportRegex */ @@ -392,21 +449,4 @@ public static function provideImportRegex(): iterable ], ]; } - - /** - * @dataProvider provideGetPackageVersion - */ - public function testGetPackageVersion(string $url, ?string $expected) - { - $resolver = new JsDelivrEsmResolver(); - - $this->assertSame($expected, $resolver->getPackageVersion($url)); - } - - public static function provideGetPackageVersion(): iterable - { - yield 'with no result' => ['https://cdn.jsdelivr.net/npm/lodash.js/+esm', null]; - yield 'with a package name' => ['https://cdn.jsdelivr.net/npm/lodash.js@1.2.3/+esm', '1.2.3']; - yield 'with a dash in the package_name' => ['https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.7/+esm', '2.11.7']; - } } diff --git a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JspmResolverTest.php b/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JspmResolverTest.php deleted file mode 100644 index aa90991141454..0000000000000 --- a/src/Symfony/Component/AssetMapper/Tests/ImportMap/Resolver/JspmResolverTest.php +++ /dev/null @@ -1,178 +0,0 @@ - - * - * 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\Resolver; - -use PHPUnit\Framework\TestCase; -use Symfony\Component\AssetMapper\ImportMap\ImportMapManager; -use Symfony\Component\AssetMapper\ImportMap\PackageRequireOptions; -use Symfony\Component\AssetMapper\ImportMap\Resolver\JspmResolver; -use Symfony\Component\HttpClient\MockHttpClient; -use Symfony\Component\HttpClient\Response\MockResponse; - -class JspmResolverTest extends TestCase -{ - /** - * @dataProvider provideResolvePackagesTests - */ - public function testResolvePackages(array $packages, array $expectedInstallRequest, array $responseMap, array $expectedResolvedPackages, array $expectedDownloadedFiles) - { - $expectedRequestBody = [ - 'install' => $expectedInstallRequest, - 'flattenScope' => true, - 'env' => ['browser', 'module', 'production'], - ]; - $responseData = [ - 'map' => [ - 'imports' => $responseMap, - ], - ]; - - $responses = []; - $responses[] = function ($method, $url, $options) use ($responseData, $expectedRequestBody) { - $this->assertSame('POST', $method); - $this->assertSame('https://api.jspm.io/generate', $url); - $this->assertSame($expectedRequestBody, json_decode($options['body'], true)); - - return new MockResponse(json_encode($responseData)); - }; - // mock the "file download" requests - foreach ($expectedDownloadedFiles as $file) { - $responses[] = new MockResponse(sprintf('contents of %s', $file)); - } - - $httpClient = new MockHttpClient($responses); - - $provider = new JspmResolver($httpClient, ImportMapManager::PROVIDER_JSPM); - $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]['url'], $package->url); - } - } - - public static function provideResolvePackagesTests(): iterable - { - yield 'require single lodash package' => [ - 'packages' => [new PackageRequireOptions('lodash')], - 'expectedInstallRequest' => ['lodash'], - 'responseMap' => [ - 'lodash' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - ], - 'expectedDownloadedFiles' => [], - ]; - - yield 'require two packages' => [ - 'packages' => [new PackageRequireOptions('lodash'), new PackageRequireOptions('cowsay')], - 'expectedInstallRequest' => ['lodash', 'cowsay'], - 'responseMap' => [ - 'lodash' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - 'cowsay' => 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.js', - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - 'cowsay' => [ - 'url' => 'https://ga.jspm.io/npm:cowsay@4.5.6/cowsay.js', - ], - ], - 'expectedDownloadedFiles' => [], - ]; - - yield 'single_package_that_returns_as_two' => [ - 'packages' => [new PackageRequireOptions('lodash')], - 'expectedInstallRequest' => ['lodash'], - 'responseMap' => [ - 'lodash' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - 'lodash-dependency' => 'https://ga.jspm.io/npm:lodash-dependency@9.8.7/lodash-dependency.js', - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - 'lodash-dependency' => [ - 'url' => 'https://ga.jspm.io/npm:lodash-dependency@9.8.7/lodash-dependency.js', - ], - ], - 'expectedDownloadedFiles' => [], - ]; - - yield 'single_package_with_version_constraint' => [ - 'packages' => [new PackageRequireOptions('lodash', '^1.2.3')], - 'expectedInstallRequest' => ['lodash@^1.2.3'], - 'responseMap' => [ - 'lodash' => 'https://ga.jspm.io/npm:lodash@1.2.7/lodash.js', - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.7/lodash.js', - ], - ], - 'expectedDownloadedFiles' => [], - ]; - - yield 'single_package_that_downloads' => [ - 'packages' => [new PackageRequireOptions('lodash', download: true)], - 'expectedInstallRequest' => ['lodash'], - 'responseMap' => [ - 'lodash' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - 'downloaded_to' => 'vendor/lodash.js', - ], - ], - 'expectedDownloadedFiles' => [ - 'assets/vendor/lodash.js', - ], - ]; - - yield 'single_package_with_jspm_custom_registry' => [ - 'packages' => [new PackageRequireOptions('lodash', registryName: 'jspm')], - 'expectedInstallRequest' => ['jspm:lodash'], - 'responseMap' => [ - 'lodash' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - 'expectedResolvedPackages' => [ - 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', - ], - ], - 'expectedDownloadedFiles' => [], - ]; - } - - /** - * @dataProvider provideGetPackageVersion - */ - public function testGetPackageVersion(string $url, ?string $expected) - { - $resolver = new JspmResolver(); - - $this->assertSame($expected, $resolver->getPackageVersion($url)); - } - - public static function provideGetPackageVersion(): iterable - { - yield 'with no result' => ['https://ga.jspm.io/npm:lodash/lodash.js', null]; - yield 'with a package name' => ['https://ga.jspm.io/npm:lodash@1.2.3/lodash.js', '1.2.3']; - yield 'with a dash in the package_name' => ['https://ga.jspm.io/npm:lodash-dependency@9.8.7/lodash-dependency.js', '9.8.7']; - } -} diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/AssetMapperTestAppKernel.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/AssetMapperTestAppKernel.php index 31641188b0fa8..03b8212518094 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'], + 'paths' => ['dir1', 'dir2', 'assets/vendor'], ], 'test' => true, ]); diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php new file mode 100644 index 0000000000000..564dfcb1286d3 --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/installed.php @@ -0,0 +1,12 @@ + + array ( + 'version' => '3.2.1', + 'path' => 'stimulus.js', + ), + '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.js new file mode 100644 index 0000000000000..cc0f2d7280f3b --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/lodash.js @@ -0,0 +1 @@ +console.log("lodash.js"); \ No newline at end of file diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/stimulus.js b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/stimulus.js new file mode 100644 index 0000000000000..75fe110601c1c --- /dev/null +++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/assets/vendor/stimulus.js @@ -0,0 +1 @@ +console.log("stimulus.js"); \ No newline at end of file diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php index c563f9b07282d..5c3ce6a1e535b 100644 --- a/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php +++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/importmap.php @@ -11,10 +11,10 @@ return [ '@hotwired/stimulus' => [ - 'url' => 'https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js', + 'version' => '3.2.1', ], 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', + 'version' => '4.17.21', ], 'file6' => [ 'path' => 'subdir/file6.js', diff --git a/src/Symfony/Component/AssetMapper/Tests/fixtures/importmaps/importmap.php b/src/Symfony/Component/AssetMapper/Tests/fixtures/importmaps/importmap.php index d63a73a2cad00..49390b47cb396 100644 --- a/src/Symfony/Component/AssetMapper/Tests/fixtures/importmaps/importmap.php +++ b/src/Symfony/Component/AssetMapper/Tests/fixtures/importmaps/importmap.php @@ -11,11 +11,10 @@ return [ '@hotwired/stimulus' => [ - 'url' => 'https://unpkg.com/@hotwired/stimulus@3.2.1/dist/stimulus.js', + 'version' => '3.2.1', ], 'lodash' => [ - 'url' => 'https://ga.jspm.io/npm:lodash@4.17.21/lodash.js', - 'downloaded_to' => 'vendor/lodash.js', + 'version' => '4.17.21', ], 'app' => [ 'path' => 'app.js', diff --git a/src/Symfony/Component/AssetMapper/composer.json b/src/Symfony/Component/AssetMapper/composer.json index 0c0f82bb816bf..33b0a2e89367d 100644 --- a/src/Symfony/Component/AssetMapper/composer.json +++ b/src/Symfony/Component/AssetMapper/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/filesystem": "^5.4|^6.0|^7.0", "symfony/http-client": "^5.4|^6.0|^7.0" }, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy