Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[AssetMapper] Add a "package specifier" to importmap in case import name != package+path #52024

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1363,8 +1363,8 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
->setArgument(1, $config['missing_import_mode']);

$container
->getDefinition('asset_mapper.importmap.remote_package_downloader')
->replaceArgument(2, $config['vendor_dir'])
->getDefinition('asset_mapper.importmap.remote_package_storage')
->replaceArgument(0, $config['vendor_dir'])
;
$container
->getDefinition('asset_mapper.mapped_asset_factory')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
use Symfony\Component\AssetMapper\MapperAwareAssetPackage;
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver;
Expand Down Expand Up @@ -145,6 +146,7 @@
->set('asset_mapper.importmap.config_reader', ImportMapConfigReader::class)
->args([
abstract_arg('importmap.php path'),
service('asset_mapper.importmap.remote_package_storage'),
])

->set('asset_mapper.importmap.manager', ImportMapManager::class)
Expand All @@ -157,11 +159,16 @@
])
->alias(ImportMapManager::class, 'asset_mapper.importmap.manager')

->set('asset_mapper.importmap.remote_package_storage', RemotePackageStorage::class)
->args([
abstract_arg('vendor directory'),
])

->set('asset_mapper.importmap.remote_package_downloader', RemotePackageDownloader::class)
->args([
service('asset_mapper.importmap.remote_package_storage'),
service('asset_mapper.importmap.config_reader'),
service('asset_mapper.importmap.resolver'),
abstract_arg('vendor directory'),
])

->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return Command::SUCCESS;
}

$displayData = array_map(fn ($importName, $packageUpdateInfo) => [
$displayData = array_map(fn (string $importName, PackageUpdateInfo $packageUpdateInfo) => [
'name' => $importName,
'current' => $packageUpdateInfo->currentVersion,
'latest' => $packageUpdateInfo->latestVersion,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,21 @@ protected function configure(): void

<info>php %command.full_name% "chart.js/auto"</info>

Or download one package/file, but alias its name in your import map:
Or require one package/file, but alias its name in your import map:

<info>php %command.full_name% "vue/dist/vue.esm-bundler.js=vue"</info>

The <info>download</info> option will download the package locally and point the
importmap to it. Use this if you want to avoid using a CDN or if you want to
ensure that the package is available even if the CDN is down.

Sometimes, a package may require other packages and multiple new items may be added
to the import map.

You can also require multiple packages at once:

<info>php %command.full_name% "lodash@^4.15" "@hotwired/stimulus"</info>

To add an importmap entry pointing to a local file, use the <info>path</info> option:

<info>php %command.full_name% "any_module_name" --path=./assets/some_file.js</info>

EOT
);
}
Expand All @@ -87,15 +87,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$path = $input->getOption('path');
if (!is_file($path)) {
$path = $this->projectDir.'/'.$path;

if (!is_file($path)) {
$io->error(sprintf('The path "%s" does not exist.', $input->getOption('path')));

return Command::FAILURE;
}
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check is done in ImportMapManager with a nice error

}

$packages = [];
Expand All @@ -110,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$packages[] = new PackageRequireOptions(
$parts['package'],
$parts['version'] ?? null,
$parts['alias'] ?? $parts['package'],
$parts['alias'] ?? null,
$path,
$input->getOption('entrypoint'),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ final class JavaScriptImportPathCompiler implements AssetCompilerInterface
{
use AssetCompilerPathResolverTrait;

// https://regex101.com/r/5Q38tj/1
private const IMPORT_PATTERN = '/(?:import\s+(?:(?:\*\s+as\s+\w+|[\w\s{},*]+)\s+from\s+)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]?/m';
// https://regex101.com/r/fquriB/1
private const IMPORT_PATTERN = '/(?:import\s*(?:(?:\*\s*as\s+\w+|[\w\s{},*]+)\s*from\s*)?|\bimport\()\s*[\'"`](\.\/[^\'"`]+|(\.\.\/)*[^\'"`]+)[\'"`]\s*[;\)]?/m';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered with a test


public function __construct(
private readonly ImportMapManager $importMapManager,
Expand Down Expand Up @@ -145,12 +145,11 @@ private function findAssetForBareImport(string $importedModule, AssetMapperInter
return null;
}

// remote entries have no MappedAsset
if ($importMapEntry->isRemotePackage()) {
return null;
if ($asset = $assetMapper->getAsset($importMapEntry->path)) {
return $asset;
}

return $assetMapper->getAsset($importMapEntry->path);
return $assetMapper->getAssetFromSourcePath($importMapEntry->path);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.g. bootstrap imports @popperjs/core. If bootstrap is ultimately preloaded, this is what allows us to also preload @popperjs/core.

}

private function findAssetForRelativeImport(string $importedModule, MappedAsset $asset, AssetMapperInterface $assetMapper): ?MappedAsset
Expand Down
16 changes: 16 additions & 0 deletions src/Symfony/Component/AssetMapper/Exception/LogicException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\AssetMapper\Exception;

class LogicException extends \LogicException implements ExceptionInterface
{
}
19 changes: 10 additions & 9 deletions src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ public function audit(): array
{
$entries = $this->configReader->getEntries();

if (!$entries) {
return [];
}

/** @var array<string, array<string, ImportMapPackageAudit>> $installed */
$packageAudits = [];

Expand All @@ -51,14 +47,19 @@ public function audit(): array
}
$version = $entry->version;

$installed[$entry->importName] ??= [];
$installed[$entry->importName][] = $version;
$packageName = $entry->getPackageName();
$installed[$packageName] ??= [];
$installed[$packageName][] = $version;

$packageVersion = $entry->importName.($version ? '@'.$version : '');
$packageAudits[$packageVersion] ??= new ImportMapPackageAudit($entry->importName, $version);
$packageVersion = $packageName.'@'.$version;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug fix - e.g. if you have chart.js/auto, $packageName will now be just chart.js, which is what we need for the API call. Covered with a test.

Also, $version is now guaranteed not null here.

$packageAudits[$packageVersion] ??= new ImportMapPackageAudit($packageName, $version);
$affectsQuery[] = $packageVersion;
}

if (!$affectsQuery) {
return [];
}

// @see https://docs.github.com/en/rest/security-advisories/global-advisories?apiVersion=2022-11-28#list-global-security-advisories
$response = $this->httpClient->request('GET', self::AUDIT_URL, [
'query' => ['affects' => implode(',', $affectsQuery)],
Expand All @@ -81,7 +82,7 @@ public function audit(): array
if (!$version || !$this->versionMatches($version, $vulnerability['vulnerable_version_range'] ?? '>= *')) {
continue;
}
$packageAudits[$package.($version ? '@'.$version : '')] = $packageAudits[$package.($version ? '@'.$version : '')]->withVulnerability(
$packageAudits[$package.'@'.$version] = $packageAudits[$package.'@'.$version]->withVulnerability(
new ImportMapPackageAuditVulnerability(
$advisory['ghsa_id'],
$advisory['cve_id'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ class ImportMapConfigReader
{
private ImportMapEntries $rootImportMapEntries;

public function __construct(private readonly string $importMapConfigPath)
{
public function __construct(
private readonly string $importMapConfigPath,
private readonly RemotePackageStorage $remotePackageStorage,
) {
}

public function getEntries(): ImportMapEntries
Expand All @@ -38,7 +40,7 @@ public function getEntries(): ImportMapEntries

$entries = new ImportMapEntries();
foreach ($importMapConfig ?? [] as $importName => $data) {
$validKeys = ['path', 'version', 'type', 'entrypoint', 'url'];
$validKeys = ['path', 'version', 'type', 'entrypoint', 'url', 'package_specifier'];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supports the rare-use case that you require a package (e.g. bootstrap) but want to have its import called something else - shoestrap - so you can import 'shoestrap'. The importmap:require command supports this directly & it's documented.

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)));
}
Expand All @@ -49,36 +51,33 @@ public function getEntries(): ImportMapEntries
}

$type = isset($data['type']) ? ImportMapType::tryFrom($data['type']) : ImportMapType::JS;
$isEntry = $data['entrypoint'] ?? false;
$isEntrypoint = $data['entrypoint'] ?? false;

if (isset($data['path'])) {
if (isset($data['version'])) {
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
}
if (isset($data['package_specifier'])) {
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "package_specifier" option.', $importName));
}

$entries->add(ImportMapEntry::createLocal($importName, $type, $data['path'], $isEntrypoint));

if ($isEntry && ImportMapType::JS !== $type) {
throw new RuntimeException(sprintf('The "entrypoint" option can only be used with the "js" type. Found "%s" in importmap.php for key "%s".', $importName, $type->value));
continue;
}

$path = $data['path'] ?? null;
$version = $data['version'] ?? null;
if (null === $version && ($data['url'] ?? null)) {
// BC layer for 6.3->6.4
$version = $this->extractVersionFromLegacyUrl($data['url']);
}
if (null === $version && null === $path) {

if (null === $version) {
throw new RuntimeException(sprintf('The importmap entry "%s" must have either a "path" or "version" option.', $importName));
}
if (null !== $version && null !== $path) {
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
}

[$packageName, $filePath] = self::splitPackageNameAndFilePath($importName);

$entries->add(new ImportMapEntry(
$importName,
path: $path,
version: $version,
type: $type,
isEntrypoint: $isEntry,
packageName: $packageName,
filePath: $filePath,
));
$packageModuleSpecifier = $data['package_specifier'] ?? $importName;
$entries->add($this->createRemoteEntry($importName, $type, $version, $packageModuleSpecifier, $isEntrypoint));
}

return $this->rootImportMapEntries = $entries;
Expand All @@ -91,19 +90,21 @@ public function writeEntries(ImportMapEntries $entries): void
$importMapConfig = [];
foreach ($entries as $entry) {
$config = [];
if ($entry->path) {
$path = $entry->path;
$config['path'] = $path;
}
if ($entry->version) {
if ($entry->isRemotePackage()) {
$config['version'] = $entry->version;
if ($entry->packageModuleSpecifier !== $entry->importName) {
$config['package_specifier'] = $entry->packageModuleSpecifier;
}
} else {
$config['path'] = $entry->path;
}
if (ImportMapType::JS !== $entry->type) {
$config['type'] = $entry->type->value;
}
if ($entry->isEntrypoint) {
$config['entrypoint'] = true;
}

$importMapConfig[$entry->importName] = $config;
}

Expand All @@ -129,6 +130,13 @@ public function writeEntries(ImportMapEntries $entries): void
EOF);
}

public function createRemoteEntry(string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint): ImportMapEntry
{
$path = $this->remotePackageStorage->getDownloadPath($packageModuleSpecifier, $type);

return ImportMapEntry::createRemote($importName, $type, $path, $version, $packageModuleSpecifier, $isEntrypoint);
}

public function getRootDirectory(): string
{
return \dirname($this->importMapConfigPath);
Expand All @@ -148,18 +156,4 @@ private function extractVersionFromLegacyUrl(string $url): ?string

return substr($url, $lastAt + 1, $nextSlash - $lastAt - 1);
}

public static function splitPackageNameAndFilePath(string $packageName): array
{
$filePath = '';
$i = strpos($packageName, '/');

if ($i && (!str_starts_with($packageName, '@') || $i = strpos($packageName, '/', $i + 1))) {
// @vendor/package/filepath or package/filepath
$filePath = substr($packageName, $i);
$packageName = substr($packageName, 0, $i);
}

return [$packageName, $filePath];
}
}
Loading