Skip to content

Commit 8ca6931

Browse files
committed
feature #52024 [AssetMapper] Add a "package specifier" to importmap in case import name != package+path (weaverryan)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [AssetMapper] Add a "package specifier" to importmap in case import name != package+path | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | yes | New feature? | yes | Deprecations? | no | Tickets | None | License | MIT Hi! Sorry for the large PR - this tightens up the component after the many recent enhancements. `tl;dr` Makes "vendor" importmap entries work identically to "local" entries when it comes to preloading (a bug). Also tightens up `ImportMapEntry`, which is a tricky class because if an entry is "remote", it has 2 extra, required, properties. Full changes: * Make ImportMapEntry stricter so that, when remote, version & packageModuleSpecifier are always set. * Set the "path" property always for ImportMapEntry, making them no different than local files through most of the system. * Related, fix a bug where if a vendor .js file imported another module, that other module was not previously preloaded when the vendor module was preloaded. * Fix a bug with downloaded package paths if you include a specific package - chart.js - and also a path - chart.js/auto. Previously, this would try to create both a file and directory called chart.js, * Fix bug where imports weren't matching if some spaces were missing Cheers! Commits ------- c80a1ab [AssetMapper] Add a "package specifier" to importmap in case import name != package+path
2 parents d30597d + c80a1ab commit 8ca6931

34 files changed

+773
-452
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,8 +1363,8 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
13631363
->setArgument(1, $config['missing_import_mode']);
13641364

13651365
$container
1366-
->getDefinition('asset_mapper.importmap.remote_package_downloader')
1367-
->replaceArgument(2, $config['vendor_dir'])
1366+
->getDefinition('asset_mapper.importmap.remote_package_storage')
1367+
->replaceArgument(0, $config['vendor_dir'])
13681368
;
13691369
$container
13701370
->getDefinition('asset_mapper.mapped_asset_factory')

src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Symfony\Component\AssetMapper\ImportMap\ImportMapRenderer;
3636
use Symfony\Component\AssetMapper\ImportMap\ImportMapUpdateChecker;
3737
use Symfony\Component\AssetMapper\ImportMap\RemotePackageDownloader;
38+
use Symfony\Component\AssetMapper\ImportMap\RemotePackageStorage;
3839
use Symfony\Component\AssetMapper\ImportMap\Resolver\JsDelivrEsmResolver;
3940
use Symfony\Component\AssetMapper\MapperAwareAssetPackage;
4041
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolver;
@@ -145,6 +146,7 @@
145146
->set('asset_mapper.importmap.config_reader', ImportMapConfigReader::class)
146147
->args([
147148
abstract_arg('importmap.php path'),
149+
service('asset_mapper.importmap.remote_package_storage'),
148150
])
149151

150152
->set('asset_mapper.importmap.manager', ImportMapManager::class)
@@ -157,11 +159,16 @@
157159
])
158160
->alias(ImportMapManager::class, 'asset_mapper.importmap.manager')
159161

162+
->set('asset_mapper.importmap.remote_package_storage', RemotePackageStorage::class)
163+
->args([
164+
abstract_arg('vendor directory'),
165+
])
166+
160167
->set('asset_mapper.importmap.remote_package_downloader', RemotePackageDownloader::class)
161168
->args([
169+
service('asset_mapper.importmap.remote_package_storage'),
162170
service('asset_mapper.importmap.config_reader'),
163171
service('asset_mapper.importmap.resolver'),
164-
abstract_arg('vendor directory'),
165172
])
166173

167174
->set('asset_mapper.importmap.resolver', JsDelivrEsmResolver::class)

src/Symfony/Component/AssetMapper/Command/ImportMapOutdatedCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7373
return Command::SUCCESS;
7474
}
7575

76-
$displayData = array_map(fn ($importName, $packageUpdateInfo) => [
76+
$displayData = array_map(fn (string $importName, PackageUpdateInfo $packageUpdateInfo) => [
7777
'name' => $importName,
7878
'current' => $packageUpdateInfo->currentVersion,
7979
'latest' => $packageUpdateInfo->latestVersion,

src/Symfony/Component/AssetMapper/Command/ImportMapRequireCommand.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,21 +54,21 @@ protected function configure(): void
5454
5555
<info>php %command.full_name% "chart.js/auto"</info>
5656
57-
Or download one package/file, but alias its name in your import map:
57+
Or require one package/file, but alias its name in your import map:
5858
5959
<info>php %command.full_name% "vue/dist/vue.esm-bundler.js=vue"</info>
6060
61-
The <info>download</info> option will download the package locally and point the
62-
importmap to it. Use this if you want to avoid using a CDN or if you want to
63-
ensure that the package is available even if the CDN is down.
64-
6561
Sometimes, a package may require other packages and multiple new items may be added
6662
to the import map.
6763
6864
You can also require multiple packages at once:
6965
7066
<info>php %command.full_name% "lodash@^4.15" "@hotwired/stimulus"</info>
7167
68+
To add an importmap entry pointing to a local file, use the <info>path</info> option:
69+
70+
<info>php %command.full_name% "any_module_name" --path=./assets/some_file.js</info>
71+
7272
EOT
7373
);
7474
}
@@ -87,15 +87,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8787
}
8888

8989
$path = $input->getOption('path');
90-
if (!is_file($path)) {
91-
$path = $this->projectDir.'/'.$path;
92-
93-
if (!is_file($path)) {
94-
$io->error(sprintf('The path "%s" does not exist.', $input->getOption('path')));
95-
96-
return Command::FAILURE;
97-
}
98-
}
9990
}
10091

10192
$packages = [];
@@ -110,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
110101
$packages[] = new PackageRequireOptions(
111102
$parts['package'],
112103
$parts['version'] ?? null,
113-
$parts['alias'] ?? $parts['package'],
104+
$parts['alias'] ?? null,
114105
$path,
115106
$input->getOption('entrypoint'),
116107
);

src/Symfony/Component/AssetMapper/Compiler/JavaScriptImportPathCompiler.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ final class JavaScriptImportPathCompiler implements AssetCompilerInterface
2828
{
2929
use AssetCompilerPathResolverTrait;
3030

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

3434
public function __construct(
3535
private readonly ImportMapManager $importMapManager,
@@ -145,12 +145,11 @@ private function findAssetForBareImport(string $importedModule, AssetMapperInter
145145
return null;
146146
}
147147

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

153-
return $assetMapper->getAsset($importMapEntry->path);
152+
return $assetMapper->getAssetFromSourcePath($importMapEntry->path);
154153
}
155154

156155
private function findAssetForRelativeImport(string $importedModule, MappedAsset $asset, AssetMapperInterface $assetMapper): ?MappedAsset
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\AssetMapper\Exception;
13+
14+
class LogicException extends \LogicException implements ExceptionInterface
15+
{
16+
}

src/Symfony/Component/AssetMapper/ImportMap/ImportMapAuditor.php

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@ public function audit(): array
3535
{
3636
$entries = $this->configReader->getEntries();
3737

38-
if (!$entries) {
39-
return [];
40-
}
41-
4238
/** @var array<string, array<string, ImportMapPackageAudit>> $installed */
4339
$packageAudits = [];
4440

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

54-
$installed[$entry->importName] ??= [];
55-
$installed[$entry->importName][] = $version;
50+
$packageName = $entry->getPackageName();
51+
$installed[$packageName] ??= [];
52+
$installed[$packageName][] = $version;
5653

57-
$packageVersion = $entry->importName.($version ? '@'.$version : '');
58-
$packageAudits[$packageVersion] ??= new ImportMapPackageAudit($entry->importName, $version);
54+
$packageVersion = $packageName.'@'.$version;
55+
$packageAudits[$packageVersion] ??= new ImportMapPackageAudit($packageName, $version);
5956
$affectsQuery[] = $packageVersion;
6057
}
6158

59+
if (!$affectsQuery) {
60+
return [];
61+
}
62+
6263
// @see https://docs.github.com/en/rest/security-advisories/global-advisories?apiVersion=2022-11-28#list-global-security-advisories
6364
$response = $this->httpClient->request('GET', self::AUDIT_URL, [
6465
'query' => ['affects' => implode(',', $affectsQuery)],
@@ -81,7 +82,7 @@ public function audit(): array
8182
if (!$version || !$this->versionMatches($version, $vulnerability['vulnerable_version_range'] ?? '>= *')) {
8283
continue;
8384
}
84-
$packageAudits[$package.($version ? '@'.$version : '')] = $packageAudits[$package.($version ? '@'.$version : '')]->withVulnerability(
85+
$packageAudits[$package.'@'.$version] = $packageAudits[$package.'@'.$version]->withVulnerability(
8586
new ImportMapPackageAuditVulnerability(
8687
$advisory['ghsa_id'],
8788
$advisory['cve_id'],

src/Symfony/Component/AssetMapper/ImportMap/ImportMapConfigReader.php

Lines changed: 35 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ class ImportMapConfigReader
2323
{
2424
private ImportMapEntries $rootImportMapEntries;
2525

26-
public function __construct(private readonly string $importMapConfigPath)
27-
{
26+
public function __construct(
27+
private readonly string $importMapConfigPath,
28+
private readonly RemotePackageStorage $remotePackageStorage,
29+
) {
2830
}
2931

3032
public function getEntries(): ImportMapEntries
@@ -38,7 +40,7 @@ public function getEntries(): ImportMapEntries
3840

3941
$entries = new ImportMapEntries();
4042
foreach ($importMapConfig ?? [] as $importName => $data) {
41-
$validKeys = ['path', 'version', 'type', 'entrypoint', 'url'];
43+
$validKeys = ['path', 'version', 'type', 'entrypoint', 'url', 'package_specifier'];
4244
if ($invalidKeys = array_diff(array_keys($data), $validKeys)) {
4345
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)));
4446
}
@@ -49,36 +51,33 @@ public function getEntries(): ImportMapEntries
4951
}
5052

5153
$type = isset($data['type']) ? ImportMapType::tryFrom($data['type']) : ImportMapType::JS;
52-
$isEntry = $data['entrypoint'] ?? false;
54+
$isEntrypoint = $data['entrypoint'] ?? false;
55+
56+
if (isset($data['path'])) {
57+
if (isset($data['version'])) {
58+
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
59+
}
60+
if (isset($data['package_specifier'])) {
61+
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "package_specifier" option.', $importName));
62+
}
63+
64+
$entries->add(ImportMapEntry::createLocal($importName, $type, $data['path'], $isEntrypoint));
5365

54-
if ($isEntry && ImportMapType::JS !== $type) {
55-
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));
66+
continue;
5667
}
5768

58-
$path = $data['path'] ?? null;
5969
$version = $data['version'] ?? null;
6070
if (null === $version && ($data['url'] ?? null)) {
6171
// BC layer for 6.3->6.4
6272
$version = $this->extractVersionFromLegacyUrl($data['url']);
6373
}
64-
if (null === $version && null === $path) {
74+
75+
if (null === $version) {
6576
throw new RuntimeException(sprintf('The importmap entry "%s" must have either a "path" or "version" option.', $importName));
6677
}
67-
if (null !== $version && null !== $path) {
68-
throw new RuntimeException(sprintf('The importmap entry "%s" cannot have both a "path" and "version" option.', $importName));
69-
}
7078

71-
[$packageName, $filePath] = self::splitPackageNameAndFilePath($importName);
72-
73-
$entries->add(new ImportMapEntry(
74-
$importName,
75-
path: $path,
76-
version: $version,
77-
type: $type,
78-
isEntrypoint: $isEntry,
79-
packageName: $packageName,
80-
filePath: $filePath,
81-
));
79+
$packageModuleSpecifier = $data['package_specifier'] ?? $importName;
80+
$entries->add($this->createRemoteEntry($importName, $type, $version, $packageModuleSpecifier, $isEntrypoint));
8281
}
8382

8483
return $this->rootImportMapEntries = $entries;
@@ -91,19 +90,21 @@ public function writeEntries(ImportMapEntries $entries): void
9190
$importMapConfig = [];
9291
foreach ($entries as $entry) {
9392
$config = [];
94-
if ($entry->path) {
95-
$path = $entry->path;
96-
$config['path'] = $path;
97-
}
98-
if ($entry->version) {
93+
if ($entry->isRemotePackage()) {
9994
$config['version'] = $entry->version;
95+
if ($entry->packageModuleSpecifier !== $entry->importName) {
96+
$config['package_specifier'] = $entry->packageModuleSpecifier;
97+
}
98+
} else {
99+
$config['path'] = $entry->path;
100100
}
101101
if (ImportMapType::JS !== $entry->type) {
102102
$config['type'] = $entry->type->value;
103103
}
104104
if ($entry->isEntrypoint) {
105105
$config['entrypoint'] = true;
106106
}
107+
107108
$importMapConfig[$entry->importName] = $config;
108109
}
109110

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

133+
public function createRemoteEntry(string $importName, ImportMapType $type, string $version, string $packageModuleSpecifier, bool $isEntrypoint): ImportMapEntry
134+
{
135+
$path = $this->remotePackageStorage->getDownloadPath($packageModuleSpecifier, $type);
136+
137+
return ImportMapEntry::createRemote($importName, $type, $path, $version, $packageModuleSpecifier, $isEntrypoint);
138+
}
139+
132140
public function getRootDirectory(): string
133141
{
134142
return \dirname($this->importMapConfigPath);
@@ -148,18 +156,4 @@ private function extractVersionFromLegacyUrl(string $url): ?string
148156

149157
return substr($url, $lastAt + 1, $nextSlash - $lastAt - 1);
150158
}
151-
152-
public static function splitPackageNameAndFilePath(string $packageName): array
153-
{
154-
$filePath = '';
155-
$i = strpos($packageName, '/');
156-
157-
if ($i && (!str_starts_with($packageName, '@') || $i = strpos($packageName, '/', $i + 1))) {
158-
// @vendor/package/filepath or package/filepath
159-
$filePath = substr($packageName, $i);
160-
$packageName = substr($packageName, 0, $i);
161-
}
162-
163-
return [$packageName, $filePath];
164-
}
165159
}

0 commit comments

Comments
 (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