Skip to content

Commit

Permalink
[AssetMapper] Avoid storing & caching MappedAsset objects
Browse files Browse the repository at this point in the history
The problem is that these could get out of date, allowing asset Foo to have a
JavaScript import for the MappedAsset for Bar, but it is out of date because
Bar has since changed.
  • Loading branch information
weaverryan committed Nov 10, 2023
1 parent 5aaa534 commit bb5ef88
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac
$addToImportMap = $isRelativeImport;
$asset->addJavaScriptImport(new JavaScriptImport(
$addToImportMap ? $dependentAsset->publicPathWithoutDigest : $importedModule,
$dependentAsset,
$dependentAsset->logicalPath,
$dependentAsset->sourcePath,
$isLazy,
$addToImportMap,
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private function collectResourcesFromAsset(MappedAsset $mappedAsset): array
}

foreach ($mappedAsset->getJavaScriptImports() as $import) {
$resources[] = new FileExistenceResource($import->asset->sourcePath);
$resources[] = new FileExistenceResource($import->assetSourcePath);
}

return $resources;
Expand Down
15 changes: 12 additions & 3 deletions src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,15 @@ private function addImplicitEntries(ImportMapEntry $entry, array $currentImportE

// check if this import requires an automatic importmap entry
if ($javaScriptImport->addImplicitlyToImportMap) {
if (!$importedAsset = $this->assetMapper->getAsset($javaScriptImport->assetLogicalPath)) {
// should not happen at this point, unless something added a bogus JavaScriptImport to this asset
throw new LogicException(sprintf('Cannot find imported JavaScript asset "%s" in asset mapper.', $javaScriptImport->assetLogicalPath));
}

$nextEntry = ImportMapEntry::createLocal(
$importName,
ImportMapType::tryFrom($javaScriptImport->asset->publicExtension) ?: ImportMapType::JS,
$javaScriptImport->asset->logicalPath,
ImportMapType::tryFrom($importedAsset->publicExtension) ?: ImportMapType::JS,
$importedAsset->logicalPath,
false,
);

Expand Down Expand Up @@ -223,7 +228,11 @@ private function findEagerImports(MappedAsset $asset): array
$dependencies[] = $javaScriptImport->importName;

// Follow its imports!
$dependencies = array_merge($dependencies, $this->findEagerImports($javaScriptImport->asset));
if (!$nextAsset = $this->assetMapper->getAsset($javaScriptImport->assetLogicalPath)) {
// should not happen at this point, unless something added a bogus JavaScriptImport to this asset
throw new LogicException(sprintf('Cannot find imported JavaScript asset "%s" in asset mapper.', $javaScriptImport->assetLogicalPath));
}
$dependencies = array_merge($dependencies, $this->findEagerImports($nextAsset));
}

return $dependencies;
Expand Down
11 changes: 5 additions & 6 deletions src/Symfony/Component/AssetMapper/ImportMap/JavaScriptImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,20 @@

namespace Symfony\Component\AssetMapper\ImportMap;

use Symfony\Component\AssetMapper\MappedAsset;

/**
* Represents a module that was imported by a JavaScript file.
*/
final class JavaScriptImport
{
/**
* @param string $importName The name of the import needed in the importmap, e.g. "/foo.js" or "react"
* @param MappedAsset $asset The asset that was imported
* @param bool $addImplicitlyToImportMap Whether this import should be added to the importmap automatically
* @param string $importName The name of the import needed in the importmap, e.g. "/foo.js" or "react"
* @param string $assetLogicalPath Logical path to the mapped ass that was imported
* @param bool $addImplicitlyToImportMap Whether this import should be added to the importmap automatically
*/
public function __construct(
public readonly string $importName,
public readonly MappedAsset $asset,
public readonly string $assetLogicalPath,
public readonly string $assetSourcePath,
public readonly bool $isLazy = false,
public bool $addImplicitlyToImportMap = false,
) {
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/AssetMapper/MappedAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public function __construct(
}

/**
* Assets that the content of this asset depends on - for internal caching.
*
* @return MappedAsset[]
*/
public function getDependencies(): array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function testCompileFindsCorrectImports(string $input, array $expectedJav
->method('getAsset')
->willReturnCallback(function ($path) {
return match ($path) {
'module_in_importmap_local_asset.js' => new MappedAsset('module_in_importmap_local_asset.js', publicPathWithoutDigest: '/assets/module_in_importmap_local_asset.js'),
'module_in_importmap_local_asset.js' => new MappedAsset('module_in_importmap_local_asset.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/module_in_importmap_local_asset.js'),
default => null,
};
});
Expand All @@ -67,11 +67,11 @@ public function testCompileFindsCorrectImports(string $input, array $expectedJav
->method('getAssetFromSourcePath')
->willReturnCallback(function ($path) {
return match ($path) {
'/project/assets/other.js' => new MappedAsset('other.js', publicPathWithoutDigest: '/assets/other.js'),
'/project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
'/project/assets/styles.css' => new MappedAsset('styles.css', publicPathWithoutDigest: '/assets/styles.css'),
'/project/assets/vendor/module_in_importmap_remote.js' => new MappedAsset('module_in_importmap_remote.js', publicPathWithoutDigest: '/assets/module_in_importmap_remote.js'),
'/project/assets/vendor/@popperjs/core.js' => new MappedAsset('assets/vendor/@popperjs/core.js', publicPathWithoutDigest: '/assets/@popperjs/core.js'),
'/project/assets/other.js' => new MappedAsset('other.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/other.js'),
'/project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
'/project/assets/styles.css' => new MappedAsset('styles.css', '/can/be/anything.js', publicPathWithoutDigest: '/assets/styles.css'),
'/project/assets/vendor/module_in_importmap_remote.js' => new MappedAsset('module_in_importmap_remote.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/module_in_importmap_remote.js'),
'/project/assets/vendor/@popperjs/core.js' => new MappedAsset('assets/vendor/@popperjs/core.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/@popperjs/core.js'),
default => null,
};
});
Expand All @@ -81,7 +81,7 @@ public function testCompileFindsCorrectImports(string $input, array $expectedJav
$this->assertSame($input, $compiler->compile($input, $asset, $assetMapper));
$actualImports = [];
foreach ($asset->getJavaScriptImports() as $import) {
$actualImports[$import->importName] = ['lazy' => $import->isLazy, 'asset' => $import->asset->logicalPath, 'add' => $import->addImplicitlyToImportMap];
$actualImports[$import->importName] = ['lazy' => $import->isLazy, 'asset' => $import->assetLogicalPath, 'add' => $import->addImplicitlyToImportMap];
}

$this->assertEquals($expectedJavaScriptImports, $actualImports);
Expand Down Expand Up @@ -304,9 +304,9 @@ public function testCompileFindsRelativePathsViaSourcePath()
->method('getAssetFromSourcePath')
->willReturnCallback(function ($path) {
return match ($path) {
'/project/assets/other.js' => new MappedAsset('other.js', publicPathWithoutDigest: '/assets/other.js'),
'/project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
'/project/root_asset.js' => new MappedAsset('root_asset.js', publicPathWithoutDigest: '/assets/root_asset.js'),
'/project/assets/other.js' => new MappedAsset('other.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/other.js'),
'/project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
'/project/root_asset.js' => new MappedAsset('root_asset.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/root_asset.js'),
default => throw new \RuntimeException(sprintf('Unexpected source path "%s"', $path)),
};
});
Expand All @@ -320,9 +320,9 @@ public function testCompileFindsRelativePathsViaSourcePath()
$compiler = new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class));
$compiler->compile($input, $inputAsset, $assetMapper);
$this->assertCount(3, $inputAsset->getJavaScriptImports());
$this->assertSame('other.js', $inputAsset->getJavaScriptImports()[0]->asset->logicalPath);
$this->assertSame('subdir/foo.js', $inputAsset->getJavaScriptImports()[1]->asset->logicalPath);
$this->assertSame('root_asset.js', $inputAsset->getJavaScriptImports()[2]->asset->logicalPath);
$this->assertSame('other.js', $inputAsset->getJavaScriptImports()[0]->assetLogicalPath);
$this->assertSame('subdir/foo.js', $inputAsset->getJavaScriptImports()[1]->assetLogicalPath);
$this->assertSame('root_asset.js', $inputAsset->getJavaScriptImports()[2]->assetLogicalPath);
}

public function testCompileFindsRelativePathsWithWindowsPathsViaSourcePath()
Expand All @@ -337,9 +337,9 @@ public function testCompileFindsRelativePathsWithWindowsPathsViaSourcePath()
->method('getAssetFromSourcePath')
->willReturnCallback(function ($path) {
return match ($path) {
'C://project/assets/other.js' => new MappedAsset('other.js', publicPathWithoutDigest: '/assets/other.js'),
'C://project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
'C://project/root_asset.js' => new MappedAsset('root_asset.js', publicPathWithoutDigest: '/assets/root_asset.js'),
'C://project/assets/other.js' => new MappedAsset('other.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/other.js'),
'C://project/assets/subdir/foo.js' => new MappedAsset('subdir/foo.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/subdir/foo.js'),
'C://project/root_asset.js' => new MappedAsset('root_asset.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/root_asset.js'),
default => throw new \RuntimeException(sprintf('Unexpected source path "%s"', $path)),
};
});
Expand All @@ -366,7 +366,7 @@ public function testImportPathsCanUpdateForDifferentPublicPath(string $input, st
$asset = new MappedAsset('app.js', '/path/to/assets/app.js', publicPathWithoutDigest: $inputAssetPublicPath);

$assetMapper = $this->createMock(AssetMapperInterface::class);
$importedAsset = new MappedAsset('anything', publicPathWithoutDigest: $importedPublicPath);
$importedAsset = new MappedAsset('anything', '/can/be/anything.js', publicPathWithoutDigest: $importedPublicPath);
$assetMapper->expects($this->once())
->method('getAssetFromSourcePath')
->willReturn($importedAsset);
Expand Down Expand Up @@ -436,7 +436,7 @@ public function testCompileHandlesCircularRelativeAssets()
$input = 'import "./other.js";';
$compiler->compile($input, $appAsset, $assetMapper);
$this->assertCount(1, $appAsset->getJavaScriptImports());
$this->assertSame($otherAsset, $appAsset->getJavaScriptImports()[0]->asset);
$this->assertSame($otherAsset->logicalPath, $appAsset->getJavaScriptImports()[0]->assetLogicalPath);
}

public function testCompileHandlesCircularBareImportAssets()
Expand Down Expand Up @@ -464,7 +464,7 @@ public function testCompileHandlesCircularBareImportAssets()
$input = 'import "@popperjs/core";';
$compiler->compile($input, $bootstrapAsset, $assetMapper);
$this->assertCount(1, $bootstrapAsset->getJavaScriptImports());
$this->assertSame($popperAsset, $bootstrapAsset->getJavaScriptImports()[0]->asset);
$this->assertSame($popperAsset->logicalPath, $bootstrapAsset->getJavaScriptImports()[0]->assetLogicalPath);
}

/**
Expand All @@ -490,7 +490,7 @@ public function testMissingImportMode(string $sourceLogicalName, string $input,
->method('getAssetFromSourcePath')
->willReturnCallback(function ($sourcePath) {
return match ($sourcePath) {
'/path/to/other.js' => new MappedAsset('other.js', publicPathWithoutDigest: '/assets/other.js'),
'/path/to/other.js' => new MappedAsset('other.js', '/can/be/anything.js', publicPathWithoutDigest: '/assets/other.js'),
default => null,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public function testAssetConfigCacheResourceContainsDependencies()
$deeplyNestedAsset = new MappedAsset('file4.js', realpath(__DIR__.'/../Fixtures/dir2/file4.js'));

$file6Asset = new MappedAsset('file6.js', realpath(__DIR__.'/../Fixtures/dir2/subdir/file6.js'));
$deeplyNestedAsset->addJavaScriptImport(new JavaScriptImport('file6', asset: $file6Asset));
$deeplyNestedAsset->addJavaScriptImport(new JavaScriptImport('file6', assetLogicalPath: $file6Asset->logicalPath, assetSourcePath: $file6Asset->sourcePath));

$dependentOnContentAsset->addDependency($deeplyNestedAsset);
$mappedAsset->addDependency($dependentOnContentAsset);
Expand Down

0 comments on commit bb5ef88

Please sign in to comment.