Skip to content

Commit 0b78897

Browse files
feature #51729 [AssetMapper] Allow simple, relative paths in importmap.php (weaverryan)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [AssetMapper] Allow simple, relative paths in importmap.php | Q | A | ------------- | --- | Branch? | 6.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | None | License | MIT | Doc PR | TODO This PR is based on top of #51543 - so that needs to be merged first. Currently, the `path` key in `importmap.php` MUST be the logic path to an file. Especially when trying to refer to assets in a bundle, this forces us to use paths that... don't look very obvious to users - e.g. ```php 'app' => [ 'path' => 'app.js', ], '`@symfony`/stimulus-bundle' => [ 'path' => '`@symfony`/stimulus-bundle/loader.js', ], ``` This PR adds support for relative paths (starting with `.`). This means the `importmap.php` file can look like this now: ```php 'app' => [ 'path' => './assets/app.js', ], '`@symfony`/stimulus-bundle' => [ 'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js', ], ``` Those paths are now simple. One less thing to explain to people. Commits ------- 978b14d [AssetMapper] Allow simple, relative paths in importmap.php
2 parents 507ef2e + 978b14d commit 0b78897

File tree

6 files changed

+150
-28
lines changed

6 files changed

+150
-28
lines changed

src/Symfony/Component/AssetMapper/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Mark the component as non experimental
88
* Add CSS support to the importmap
99
* Add "entrypoints" concept to the importmap
10+
* Allow relative path strings in the importmap
1011
* Add `PreAssetsCompileEvent` event when running `asset-map:compile`
1112
* Add support for importmap paths to use the Asset component (for subdirectories)
1213
* Removed the `importmap:export` command

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,9 @@ public function writeEntries(ImportMapEntries $entries): void
107107
108108
EOF);
109109
}
110+
111+
public function getRootDirectory(): string
112+
{
113+
return \dirname($this->importMapConfigPath);
114+
}
110115
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
final class ImportMapEntry
2020
{
2121
public function __construct(
22+
public readonly string $importName,
2223
/**
23-
* The logical path to this asset if local or downloaded.
24+
* The path to the asset if local or downloaded.
2425
*/
25-
public readonly string $importName,
2626
public readonly ?string $path = null,
2727
public readonly ?string $url = null,
2828
public readonly bool $isDownloaded = false,

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

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ public function downloadMissingPackages(): array
108108
$downloadedPackages = [];
109109

110110
foreach ($entries as $entry) {
111-
if (!$entry->isDownloaded || $this->assetMapper->getAsset($entry->path)) {
111+
if (!$entry->isDownloaded || $this->findAsset($entry->path)) {
112112
continue;
113113
}
114114

@@ -211,7 +211,7 @@ public function getRawImportMapData(): array
211211
$rawImportMapData = [];
212212
foreach ($allEntries as $entry) {
213213
if ($entry->path) {
214-
$asset = $this->assetMapper->getAsset($entry->path);
214+
$asset = $this->findAsset($entry->path);
215215

216216
if (!$asset) {
217217
if ($entry->isDownloaded) {
@@ -330,11 +330,15 @@ private function requirePackages(array $packagesToRequire, ImportMapEntries $imp
330330
}
331331

332332
$path = $requireOptions->path;
333-
if (is_file($path)) {
334-
$path = $this->assetMapper->getAssetFromSourcePath($path)?->logicalPath;
335-
if (null === $path) {
336-
throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found in any asset map paths.', $requireOptions->path, $requireOptions->packageName));
337-
}
333+
if (!$asset = $this->findAsset($path)) {
334+
throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found: either pass the logical name of the asset or a relative path starting with "./".', $requireOptions->path, $requireOptions->packageName));
335+
}
336+
337+
$rootImportMapDir = $this->importMapConfigReader->getRootDirectory();
338+
// convert to a relative path (or fallback to the logical path)
339+
$path = $asset->logicalPath;
340+
if ($rootImportMapDir && str_starts_with(realpath($asset->sourcePath), realpath($rootImportMapDir))) {
341+
$path = './'.substr(realpath($asset->sourcePath), \strlen(realpath($rootImportMapDir)) + 1);
338342
}
339343

340344
$newEntry = new ImportMapEntry(
@@ -384,7 +388,7 @@ private function cleanupPackageFiles(ImportMapEntry $entry): void
384388
return;
385389
}
386390

387-
$asset = $this->assetMapper->getAsset($entry->path);
391+
$asset = $this->findAsset($entry->path);
388392

389393
if (!$asset) {
390394
throw new \LogicException(sprintf('The path "%s" of the package "%s" cannot be found in any asset map paths.', $entry->path, $entry->importName));
@@ -418,7 +422,7 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE
418422
return $currentImportEntries;
419423
}
420424

421-
if (!$asset = $this->assetMapper->getAsset($entry->path)) {
425+
if (!$asset = $this->findAsset($entry->path)) {
422426
// should only be possible at this point for root importmap.php entries
423427
throw new \InvalidArgumentException(sprintf('The asset "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $entry->path));
424428
}
@@ -498,7 +502,7 @@ private function findEagerEntrypointImports(string $entryName): array
498502
throw new \InvalidArgumentException(sprintf('The entrypoint "%s" is a remote package and cannot be used as an entrypoint.', $entryName));
499503
}
500504

501-
$asset = $this->assetMapper->getAsset($rootImportEntries->get($entryName)->path);
505+
$asset = $this->findAsset($rootImportEntries->get($entryName)->path);
502506
if (!$asset) {
503507
throw new \InvalidArgumentException(sprintf('The path "%s" of the entrypoint "%s" mentioned in "importmap.php" cannot be found in any asset map paths.', $rootImportEntries->get($entryName)->path, $entryName));
504508
}
@@ -529,4 +533,20 @@ private static function getImportMapTypeFromFilename(string $path): ImportMapTyp
529533
{
530534
return str_ends_with($path, '.css') ? ImportMapType::CSS : ImportMapType::JS;
531535
}
536+
537+
/**
538+
* Finds the MappedAsset allowing for a "logical path", relative or absolute filesystem path.
539+
*/
540+
private function findAsset(string $path): ?MappedAsset
541+
{
542+
if ($asset = $this->assetMapper->getAsset($path)) {
543+
return $asset;
544+
}
545+
546+
if (str_starts_with($path, '.')) {
547+
$path = $this->importMapConfigReader->getRootDirectory().'/'.$path;
548+
}
549+
550+
return $this->assetMapper->getAssetFromSourcePath($path);
551+
}
532552
}

src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapConfigReaderTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,10 @@ public function testGetEntriesAndWriteEntries()
102102

103103
$this->assertSame($originalImportMapData, $newImportMapData);
104104
}
105+
106+
public function testGetRootDirectory()
107+
{
108+
$configReader = new ImportMapConfigReader(__DIR__.'/../fixtures/importmap.php');
109+
$this->assertSame(__DIR__.'/../fixtures', $configReader->getRootDirectory());
110+
}
105111
}

src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapManagerTest.php

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ public function testGetRawImportMapData(array $importMapEntries, array $mappedAs
6565
$manager = $this->createImportMapManager();
6666
$this->mockImportMap($importMapEntries);
6767
$this->mockAssetMapper($mappedAssets);
68+
$this->configReader->expects($this->any())
69+
->method('getRootDirectory')
70+
->willReturn('/fake/root');
6871

6972
$this->assertEquals($expectedData, $manager->getRawImportMapData());
7073
}
@@ -255,7 +258,7 @@ public function getRawImportMapDataTests(): iterable
255258
'imports_simple' => [
256259
'path' => '/assets/imports_simple-d1g3st.js',
257260
'type' => 'js',
258-
]
261+
],
259262
],
260263
];
261264

@@ -304,6 +307,51 @@ public function getRawImportMapDataTests(): iterable
304307
],
305308
],
306309
];
310+
311+
yield 'it handles a relative path file' => [
312+
[
313+
new ImportMapEntry(
314+
'app',
315+
path: './assets/app.js',
316+
),
317+
],
318+
[
319+
new MappedAsset(
320+
'app.js',
321+
// /fake/root is the mocked root directory
322+
'/fake/root/assets/app.js',
323+
publicPath: '/assets/app.js',
324+
),
325+
],
326+
[
327+
'app' => [
328+
'path' => '/assets/app.js',
329+
'type' => 'js',
330+
],
331+
],
332+
];
333+
334+
yield 'it handles an absolute path file' => [
335+
[
336+
new ImportMapEntry(
337+
'app',
338+
path: '/some/path/assets/app.js',
339+
),
340+
],
341+
[
342+
new MappedAsset(
343+
'app.js',
344+
'/some/path/assets/app.js',
345+
publicPath: '/assets/app.js',
346+
),
347+
],
348+
[
349+
'app' => [
350+
'path' => '/assets/app.js',
351+
'type' => 'js',
352+
],
353+
],
354+
];
307355
}
308356

309357
public function testGetRawImportDataUsesCacheFile()
@@ -609,19 +657,22 @@ public function testRequire(array $packages, int $expectedProviderPackageArgumen
609657
foreach ($expectedDownloadedFiles as $file => $contents) {
610658
$expectedPath = self::$writableRoot.'/assets/vendor/'.$file;
611659
if (realpath($expectedPath) === realpath($sourcePath)) {
612-
return new MappedAsset('vendor/'.$file);
660+
return new MappedAsset('vendor/'.$file, $sourcePath);
613661
}
614662
}
615663

616664
if (str_ends_with($sourcePath, 'some_file.js')) {
617665
// physical file we point to in one test
618-
return new MappedAsset('some_file.js');
666+
return new MappedAsset('some_file.js', $sourcePath);
619667
}
620668

621669
return null;
622670
})
623671
;
624672

673+
$this->configReader->expects($this->any())
674+
->method('getRootDirectory')
675+
->willReturn(self::$writableRoot);
625676
$this->configReader->expects($this->once())
626677
->method('getEntries')
627678
->willReturn(new ImportMapEntries())
@@ -789,7 +840,8 @@ public static function getRequirePackageTests(): iterable
789840
'resolvedPackages' => [],
790841
'expectedImportMap' => [
791842
'some/module' => [
792-
'path' => 'some_file.js',
843+
// converted to relative path
844+
'path' => './assets/some_file.js',
793845
],
794846
],
795847
'expectedDownloadedFiles' => [],
@@ -849,7 +901,7 @@ public function testUpdateAll()
849901

850902
$this->mockAssetMapper([
851903
new MappedAsset('vendor/moo.js', self::$writableRoot.'/assets/vendor/moo.js'),
852-
]);
904+
], false);
853905
$this->assetMapper->expects($this->any())
854906
->method('getAssetFromSourcePath')
855907
->willReturnCallback(function (string $sourcePath) {
@@ -932,6 +984,9 @@ public function testUpdateWithSpecificPackages()
932984
])
933985
;
934986

987+
$this->configReader->expects($this->any())
988+
->method('getRootDirectory')
989+
->willReturn(self::$writableRoot);
935990
$this->configReader->expects($this->once())
936991
->method('writeEntries')
937992
->with($this->callback(function (ImportMapEntries $entries) {
@@ -947,10 +1002,6 @@ public function testUpdateWithSpecificPackages()
9471002
$this->mockAssetMapper([
9481003
new MappedAsset('vendor/cowsay.js', self::$writableRoot.'/assets/vendor/cowsay.js'),
9491004
]);
950-
$this->assetMapper->expects($this->once())
951-
->method('getAssetFromSourcePath')
952-
->willReturn(new MappedAsset('vendor/cowsay.js'))
953-
;
9541005

9551006
$manager->update(['cowsay']);
9561007
$actualContents = file_get_contents(self::$writableRoot.'/assets/vendor/cowsay.js');
@@ -968,13 +1019,14 @@ public function testDownloadMissingPackages()
9681019
$this->mockAssetMapper([
9691020
// fake that vendor/lodash.js exists, but not stimulus
9701021
new MappedAsset('vendor/lodash.js'),
971-
]);
972-
$this->assetMapper->expects($this->once())
1022+
], false);
1023+
$this->assetMapper->expects($this->any())
9731024
->method('getAssetFromSourcePath')
974-
->with($this->callback(function (string $sourcePath) {
975-
return str_ends_with($sourcePath, 'assets/vendor/@hotwired/stimulus.js');
976-
}))
977-
->willReturn(new MappedAsset('vendor/@hotwired/stimulus.js'))
1025+
->willReturnCallback(function (string $sourcePath) {
1026+
if (str_ends_with($sourcePath, 'assets/vendor/@hotwired/stimulus.js')) {
1027+
return new MappedAsset('vendor/@hotwired/stimulus.js');
1028+
}
1029+
})
9781030
;
9791031

9801032
$response = $this->createMock(ResponseInterface::class);
@@ -1125,7 +1177,7 @@ private function mockImportMap(array $importMapEntries): void
11251177
/**
11261178
* @param MappedAsset[] $mappedAssets
11271179
*/
1128-
private function mockAssetMapper(array $mappedAssets): void
1180+
private function mockAssetMapper(array $mappedAssets, bool $mockGetAssetFromSourcePath = true): void
11291181
{
11301182
$this->assetMapper->expects($this->any())
11311183
->method('getAsset')
@@ -1139,6 +1191,44 @@ private function mockAssetMapper(array $mappedAssets): void
11391191
return null;
11401192
})
11411193
;
1194+
1195+
if (!$mockGetAssetFromSourcePath) {
1196+
return;
1197+
}
1198+
1199+
$this->assetMapper->expects($this->any())
1200+
->method('getAssetFromSourcePath')
1201+
->willReturnCallback(function (string $sourcePath) use ($mappedAssets) {
1202+
// collapse ../ in paths and ./ in paths to mimic the realpath AssetMapper uses
1203+
$unCollapsePath = function (string $path) {
1204+
$parts = explode('/', $path);
1205+
$newParts = [];
1206+
foreach ($parts as $part) {
1207+
if ('..' === $part) {
1208+
array_pop($newParts);
1209+
1210+
continue;
1211+
}
1212+
1213+
if ('.' !== $part) {
1214+
$newParts[] = $part;
1215+
}
1216+
}
1217+
1218+
return implode('/', $newParts);
1219+
};
1220+
1221+
$sourcePath = $unCollapsePath($sourcePath);
1222+
1223+
foreach ($mappedAssets as $asset) {
1224+
if (isset($asset->sourcePath) && $unCollapsePath($asset->sourcePath) === $sourcePath) {
1225+
return $asset;
1226+
}
1227+
}
1228+
1229+
return null;
1230+
})
1231+
;
11421232
}
11431233

11441234
private function writeFile(string $filename, string $content): void

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