From 05bcb985dbad32f277a0a5b6c773eb93d2907e95 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sat, 28 Oct 2023 22:30:42 -0400 Subject: [PATCH] Adding support for AssetMapper 6.4 --- .../ReactControllerLoaderAssetCompiler.php | 16 +- .../DependencyInjection/ReactExtension.php | 30 ++-- ...ReactControllerLoaderAssetCompilerTest.php | 4 +- src/StimulusBundle/composer.json | 3 +- src/StimulusBundle/config/services.php | 10 ++ .../src/AssetMapper/AutoImportLocator.php | 72 +++++++++ .../AssetMapper/ControllersMapGenerator.php | 35 ++++- .../src/AssetMapper/MappedControllerAsset.php | 4 + .../MappedControllerAutoImport.php | 26 ++++ .../StimulusLoaderJavaScriptCompiler.php | 45 ++++-- .../DependencyInjection/StimulusExtension.php | 7 +- .../src/Twig/UxControllersTwigRuntime.php | 8 + .../src/Ux/UxPackageMetadata.php | 3 + .../AssetMapper/AutoImportLocatorTest.php | 92 +++++++++++ .../ControllerMapGeneratorTest.php | 22 +++ ...StimulusControllerLoaderFunctionalTest.php | 146 +++++++++++++----- .../StimulusLoaderJavaScriptCompilerTest.php | 6 +- .../Twig/UxControllersTwigRuntimeTest.php | 10 +- .../tests/fixtures/StimulusTestKernel.php | 4 + .../tests/fixtures/assets/controllers.json | 2 +- .../@hotwired/stimulus/stimulus.index.js | 1 + .../needed-vendor/needed-vendor.index.js | 1 + .../@scoped/needed-vendor/the/file2.css | 1 + .../assets/vendor/needed-vendor/file.css | 1 + .../needed-vendor/needed-vendor.index.js | 1 + .../tests/fixtures/importmap.php | 14 +- .../in-asset-mapper/controller_second1.css | 1 + .../tests/fixtures/legacy/importmap.php | 30 ++++ .../ux-package1/assets/dist/styles.css | 0 .../SvelteControllerLoaderAssetCompiler.php | 16 +- .../DependencyInjection/SvelteExtension.php | 18 +-- ...velteControllerLoaderAssetCompilerTest.php | 4 +- src/TwigComponent/tests/Fixtures/Kernel.php | 2 + .../Integration/ComponentExtensionTest.php | 8 +- .../VueControllerLoaderAssetCompiler.php | 16 +- .../src/DependencyInjection/VueExtension.php | 20 +-- .../VueControllerLoaderAssetCompilerTest.php | 4 +- 37 files changed, 558 insertions(+), 125 deletions(-) create mode 100644 src/StimulusBundle/src/AssetMapper/AutoImportLocator.php create mode 100644 src/StimulusBundle/src/AssetMapper/MappedControllerAutoImport.php create mode 100644 src/StimulusBundle/tests/AssetMapper/AutoImportLocatorTest.php create mode 100644 src/StimulusBundle/tests/fixtures/assets/vendor/@hotwired/stimulus/stimulus.index.js create mode 100644 src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/needed-vendor.index.js create mode 100644 src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/the/file2.css create mode 100644 src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/file.css create mode 100644 src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/needed-vendor.index.js create mode 100644 src/StimulusBundle/tests/fixtures/in-asset-mapper/controller_second1.css create mode 100644 src/StimulusBundle/tests/fixtures/legacy/importmap.php create mode 100644 src/StimulusBundle/tests/fixtures/vendor/fake-vendor/ux-package1/assets/dist/styles.css diff --git a/src/React/src/AssetMapper/ReactControllerLoaderAssetCompiler.php b/src/React/src/AssetMapper/ReactControllerLoaderAssetCompiler.php index 4cb2879e9ea..5f40016c8ab 100644 --- a/src/React/src/AssetMapper/ReactControllerLoaderAssetCompiler.php +++ b/src/React/src/AssetMapper/ReactControllerLoaderAssetCompiler.php @@ -11,10 +11,11 @@ namespace Symfony\UX\React\AssetMapper; +use Symfony\Component\AssetMapper\AssetDependency; use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; /** @@ -24,8 +25,6 @@ */ class ReactControllerLoaderAssetCompiler implements AssetCompilerInterface { - use AssetCompilerPathResolverTrait; - public function __construct( private string $controllerPath, private array $nameGlobs, @@ -41,10 +40,15 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac { $importLines = []; $componentParts = []; - $loaderPublicPath = $asset->publicPathWithoutDigest; foreach ($this->findControllerAssets($assetMapper) as $name => $mappedAsset) { - $controllerPublicPath = $mappedAsset->publicPathWithoutDigest; - $relativeImportPath = $this->createRelativePath($loaderPublicPath, $controllerPublicPath); + // @legacy: backwards compatibility with Symfony 6.3 + if (class_exists(AssetDependency::class)) { + $controllerPublicPath = $mappedAsset->publicPathWithoutDigest; + $loaderPublicPath = $asset->publicPathWithoutDigest; + $relativeImportPath = Path::makeRelative($controllerPublicPath, \dirname($loaderPublicPath)); + } else { + $relativeImportPath = Path::makeRelative($mappedAsset->sourcePath, \dirname($asset->sourcePath)); + } $controllerNameForVariable = sprintf('component_%s', \count($componentParts)); diff --git a/src/React/src/DependencyInjection/ReactExtension.php b/src/React/src/DependencyInjection/ReactExtension.php index 6d91145818d..0d6a0269780 100644 --- a/src/React/src/DependencyInjection/ReactExtension.php +++ b/src/React/src/DependencyInjection/ReactExtension.php @@ -12,7 +12,6 @@ namespace Symfony\UX\React\DependencyInjection; use Symfony\Component\AssetMapper\AssetMapperInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -44,22 +43,19 @@ public function load(array $configs, ContainerBuilder $container) ->setPublic(false) ; - // on older versions, the absence of this trait will trigger an error if the service is loaded - if (trait_exists(AssetCompilerPathResolverTrait::class)) { - $container->setDefinition('react.asset_mapper.react_controller_loader_compiler', new Definition(ReactControllerLoaderAssetCompiler::class)) - ->setArguments([ - $config['controllers_path'], - $config['name_glob'], - ]) - // run before the core JavaScript compiler - ->addTag('asset_mapper.compiler', ['priority' => 100]); - - $container->setDefinition('react.asset_mapper.replace_process_env_compiler', new Definition(ReactReplaceProcessEnvAssetCompiler::class)) - ->setArguments([ - '%kernel.debug%', - ]) - ->addTag('asset_mapper.compiler'); - } + $container->setDefinition('react.asset_mapper.react_controller_loader_compiler', new Definition(ReactControllerLoaderAssetCompiler::class)) + ->setArguments([ + $config['controllers_path'], + $config['name_glob'], + ]) + // run before the core JavaScript compiler + ->addTag('asset_mapper.compiler', ['priority' => 100]); + + $container->setDefinition('react.asset_mapper.replace_process_env_compiler', new Definition(ReactReplaceProcessEnvAssetCompiler::class)) + ->setArguments([ + '%kernel.debug%', + ]) + ->addTag('asset_mapper.compiler'); } public function prepend(ContainerBuilder $container) diff --git a/src/React/tests/AssetMapper/ReactControllerLoaderAssetCompilerTest.php b/src/React/tests/AssetMapper/ReactControllerLoaderAssetCompilerTest.php index 64353ab99f4..91fc383ab57 100644 --- a/src/React/tests/AssetMapper/ReactControllerLoaderAssetCompilerTest.php +++ b/src/React/tests/AssetMapper/ReactControllerLoaderAssetCompilerTest.php @@ -31,6 +31,7 @@ public function testCompileDynamicallyAddsContents() if (str_contains($sourcePath, 'MyReactController')) { return new MappedAsset( 'MyReactController.js', + '/project/assets/react/controllers/MyReactController.js', publicPathWithoutDigest: '/assets/react/controllers/MyReactController.js', ); } @@ -38,6 +39,7 @@ public function testCompileDynamicallyAddsContents() if (str_contains($sourcePath, 'DeeperReactController')) { return new MappedAsset( 'subdir/DeeperReactController.js', + '/project/assets/react/controllers/subdir/DeeperReactController.js', publicPathWithoutDigest: '/assets/react/controllers/subdir/DeeperReactController.js', ); } @@ -50,7 +52,7 @@ public function testCompileDynamicallyAddsContents() ['*.js'] ); - $loaderAsset = new MappedAsset('loader.js', publicPathWithoutDigest: '/assets/symfony/ux-react/loader.js'); + $loaderAsset = new MappedAsset('loader.js', '/project/assets/vendor/StimulusBundle/loader.js', publicPathWithoutDigest: '/assets/symfony/ux-react/loader.js'); $startingContents = file_get_contents(__DIR__.'/../../assets/dist/loader.js'); $compiledContents = $compiler->compile($startingContents, $loaderAsset, $assetMapper); diff --git a/src/StimulusBundle/composer.json b/src/StimulusBundle/composer.json index 56b6c609820..5af49cfe976 100644 --- a/src/StimulusBundle/composer.json +++ b/src/StimulusBundle/composer.json @@ -18,7 +18,8 @@ "symfony/dependency-injection": "^5.4|^6.0|^7.0", "symfony/finder": "^5.4|^6.0|^7.0", "symfony/http-kernel": "^5.4|^6.0|^7.0", - "twig/twig": "^2.15.3|^3.4.3" + "twig/twig": "^2.15.3|^3.4.3", + "symfony/deprecation-contracts": "^2.0|^3.0" }, "require-dev": { "symfony/asset-mapper": "^6.3|^7.0", diff --git a/src/StimulusBundle/config/services.php b/src/StimulusBundle/config/services.php index e60a5a8bd93..e5c67421f2e 100644 --- a/src/StimulusBundle/config/services.php +++ b/src/StimulusBundle/config/services.php @@ -12,6 +12,7 @@ */ use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\UX\StimulusBundle\AssetMapper\AutoImportLocator; use Symfony\UX\StimulusBundle\AssetMapper\ControllersMapGenerator; use Symfony\UX\StimulusBundle\AssetMapper\StimulusLoaderJavaScriptCompiler; use Symfony\UX\StimulusBundle\Helper\StimulusHelper; @@ -64,6 +65,15 @@ service('stimulus.asset_mapper.ux_package_reader'), abstract_arg('controller paths'), abstract_arg('controllers_json_path'), + // @legacy - only allowing null for framework-bundle 6.3 + service('stimulus.asset_mapper.auto_import_locator')->nullOnInvalid(), + ]) + + // @legacy - is removed in 6.3 + ->set('stimulus.asset_mapper.auto_import_locator', AutoImportLocator::class) + ->args([ + service('asset_mapper.importmap.config_reader'), + service('asset_mapper'), ]) ->set('stimulus.asset_mapper.loader_javascript_compiler', StimulusLoaderJavaScriptCompiler::class) diff --git a/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php b/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php new file mode 100644 index 00000000000..a5dafe59c2c --- /dev/null +++ b/src/StimulusBundle/src/AssetMapper/AutoImportLocator.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\AssetMapper; + +use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; +use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\UX\StimulusBundle\Ux\UxPackageMetadata; + +/** + * Finds the MappedAsset for an "autoimport" string. + */ +class AutoImportLocator +{ + public function __construct( + private ImportMapConfigReader $importMapConfigReader, + private AssetMapperInterface $assetMapper, + ) { + } + + // parts of this method are duplicated & adapted from UxControllersTwigRuntime + public function locateAutoImport(string $path, UxPackageMetadata $packageMetadata): MappedControllerAutoImport + { + // see if this is a mapped asset path + if ($asset = $this->assetMapper->getAsset($path)) { + return new MappedControllerAutoImport($asset->sourcePath, false); + } + + $slashPosition = strpos($path, '/'); + if (false === $slashPosition) { + throw new \LogicException(sprintf('The autoimport "%s" is not valid.', $path)); + } + + $parts = explode('/', ltrim($path, '@')); + if (2 > \count($parts)) { + throw new \LogicException(sprintf('The autoimport "%s" is not valid.', $path)); + } + $package = implode('/', \array_slice($parts, 0, 2)); + $file = implode('/', \array_slice($parts, 2)); + + if ($package === $packageMetadata->packageName) { + // this is a file local to the ux package + $filePath = $packageMetadata->packageDirectory.'/'.$file; + if (!is_file($filePath)) { + throw new \LogicException(sprintf('An "autoimport" in "controllers.json" refers to "%s". This path could not be found in the asset mapper and the file "%s" does not exist in the package path "%s". And so, the file cannot be loaded.', $path, $filePath, $packageMetadata->packageDirectory)); + } + + $asset = $this->assetMapper->getAssetFromSourcePath($filePath); + if (!$asset) { + throw new \LogicException(sprintf('An "autoimport" in "controllers.json" refers to "%s". This file was found, but the path is not in the asset mapper. And so, the file cannot be loaded. This is a misconfiguration with the bundle providing this.', $path)); + } + + return new MappedControllerAutoImport($asset->sourcePath, false); + } + + $entry = $this->importMapConfigReader->findRootImportMapEntry($path); + if (!$entry) { + throw new \LogicException(sprintf('The autoimport "%s" could not be found in importmap.php. Try running "importmap:require %s".', $path, $path)); + } + + return new MappedControllerAutoImport($path, true); + } +} diff --git a/src/StimulusBundle/src/AssetMapper/ControllersMapGenerator.php b/src/StimulusBundle/src/AssetMapper/ControllersMapGenerator.php index 5212f0dba4d..c015956f11b 100644 --- a/src/StimulusBundle/src/AssetMapper/ControllersMapGenerator.php +++ b/src/StimulusBundle/src/AssetMapper/ControllersMapGenerator.php @@ -12,7 +12,9 @@ namespace Symfony\UX\StimulusBundle\AssetMapper; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; use Symfony\Component\Finder\Finder; +use Symfony\UX\StimulusBundle\Ux\UxPackageMetadata; use Symfony\UX\StimulusBundle\Ux\UxPackageReader; /** @@ -31,6 +33,7 @@ public function __construct( private UxPackageReader $uxPackageReader, private array $controllerPaths, private string $controllersJsonPath, + private ?AutoImportLocator $autoImportLocator = null, ) { } @@ -72,7 +75,8 @@ private function loadCustomControllers(): array $name = str_replace(['_', '/'], ['-', '--'], $name); $asset = $this->assetMapper->getAssetFromSourcePath($file->getRealPath()); - $isLazy = preg_match('/\/\*\s*stimulusFetch:\s*\'lazy\'\s*\*\//i', $asset->content); + $content = $asset->content ?: file_get_contents($asset->sourcePath); + $isLazy = preg_match('/\/\*\s*stimulusFetch:\s*\'lazy\'\s*\*\//i', $content); $controllersMap[$name] = new MappedControllerAsset($asset, $isLazy); } @@ -129,10 +133,37 @@ private function loadUxControllers(): array throw new \RuntimeException(sprintf('Could not find an asset mapper path that points to the "%s" controller in package "%s", defined in controllers.json.', $controllerName, $packageMetadata->packageName)); } - $controllersMap[$controllerNormalizedName] = new MappedControllerAsset($asset, $lazy); + $autoImports = $this->collectAutoImports($localControllerConfig['autoimport'] ?? [], $packageMetadata); + + $controllersMap[$controllerNormalizedName] = new MappedControllerAsset($asset, $lazy, $autoImports); } } return $controllersMap; } + + /** + * @return MappedControllerAutoImport[] + */ + private function collectAutoImports(array $autoImports, UxPackageMetadata $currentPackageMetadata): array + { + // @legacy: Backwards compatibility with Symfony 6.3 + if (!class_exists(ImportMapGenerator::class)) { + return []; + } + if (null === $this->autoImportLocator) { + throw new \InvalidArgumentException(sprintf('The "autoImportLocator" argument to "%s" is required when using AssetMapper 6.4', self::class)); + } + + $autoImportItems = []; + foreach ($autoImports as $path => $enabled) { + if (!$enabled) { + continue; + } + + $autoImportItems[] = $this->autoImportLocator->locateAutoImport($path, $currentPackageMetadata); + } + + return $autoImportItems; + } } diff --git a/src/StimulusBundle/src/AssetMapper/MappedControllerAsset.php b/src/StimulusBundle/src/AssetMapper/MappedControllerAsset.php index 946a86adb67..b3e0f74a988 100644 --- a/src/StimulusBundle/src/AssetMapper/MappedControllerAsset.php +++ b/src/StimulusBundle/src/AssetMapper/MappedControllerAsset.php @@ -23,6 +23,10 @@ class MappedControllerAsset public function __construct( public MappedAsset $asset, public bool $isLazy, + /** + * @var MappedControllerAutoImport[] + */ + public array $autoImports = [], ) { } } diff --git a/src/StimulusBundle/src/AssetMapper/MappedControllerAutoImport.php b/src/StimulusBundle/src/AssetMapper/MappedControllerAutoImport.php new file mode 100644 index 00000000000..ee0ac86700e --- /dev/null +++ b/src/StimulusBundle/src/AssetMapper/MappedControllerAutoImport.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\AssetMapper; + +/** + * @experimental + * + * @author Ryan Weaver + */ +class MappedControllerAutoImport +{ + public function __construct( + public string $path, + public bool $isBareImport + ) { + } +} diff --git a/src/StimulusBundle/src/AssetMapper/StimulusLoaderJavaScriptCompiler.php b/src/StimulusBundle/src/AssetMapper/StimulusLoaderJavaScriptCompiler.php index 22a9908c8c6..0fc06234d03 100644 --- a/src/StimulusBundle/src/AssetMapper/StimulusLoaderJavaScriptCompiler.php +++ b/src/StimulusBundle/src/AssetMapper/StimulusLoaderJavaScriptCompiler.php @@ -14,8 +14,8 @@ use Symfony\Component\AssetMapper\AssetDependency; use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\Component\Filesystem\Path; /** * Compiles the loader.js file to dynamically import the controllers. @@ -26,8 +26,6 @@ */ class StimulusLoaderJavaScriptCompiler implements AssetCompilerInterface { - use AssetCompilerPathResolverTrait; - public function __construct( private ControllersMapGenerator $controllersMapGenerator, private bool $isDebug, @@ -44,7 +42,6 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac $importLines = []; $eagerControllerParts = []; $lazyControllers = []; - $loaderPublicPath = $asset->publicPathWithoutDigest; // add file dependencies so the cache rebuilds $asset->addFileDependency($this->controllersMapGenerator->getControllersJsonPath()); @@ -53,8 +50,16 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac } foreach ($this->controllersMapGenerator->getControllersMap() as $name => $mappedControllerAsset) { - $controllerPublicPath = $mappedControllerAsset->asset->publicPathWithoutDigest; - $relativeImportPath = $this->createRelativePath($loaderPublicPath, $controllerPublicPath); + // @legacy: backwards compatibility with Symfony 6.3 + if (class_exists(AssetDependency::class)) { + $loaderPublicPath = $asset->publicPathWithoutDigest; + $controllerPublicPath = $mappedControllerAsset->asset->publicPathWithoutDigest; + $relativeImportPath = Path::makeRelative($controllerPublicPath, \dirname($loaderPublicPath)); + } else { + $relativeImportPath = Path::makeRelative($mappedControllerAsset->asset->sourcePath, \dirname($asset->sourcePath)); + } + + $relativeImportPath = json_encode($relativeImportPath, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES); /* * The AssetDependency will already be added by AssetMapper itself when @@ -65,7 +70,7 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac * will be recalculated when the contents of any controller changes. */ if (class_exists(AssetDependency::class)) { - // Backwards compatibility with Symfony 6.3 + // @legacy: Backwards compatibility with Symfony 6.3 $asset->addDependency(new AssetDependency( $mappedControllerAsset->asset, $mappedControllerAsset->isLazy, @@ -75,18 +80,40 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac $asset->addDependency($mappedControllerAsset->asset); } + $autoImportPaths = []; + foreach ($mappedControllerAsset->autoImports as $autoImport) { + if ($autoImport->isBareImport) { + $autoImportPaths[] = json_encode($autoImport->path, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES); + } else { + $autoImportPaths[] = json_encode(Path::makeRelative($autoImport->path, \dirname($asset->sourcePath)), \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES); + } + } + if ($mappedControllerAsset->isLazy) { - $lazyControllers[] = sprintf('%s: () => import(%s)', json_encode($name), json_encode($relativeImportPath, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES)); + if (!$mappedControllerAsset->autoImports) { + $lazyControllers[] = sprintf('%s: () => import(%s)', json_encode($name), $relativeImportPath); + } else { + // import $relativeImportPath and also the auto-imports + // and use a Promise.all() to wait for all of them + $lazyControllers[] = sprintf('%s: () => Promise.all([import(%s), %s]).then((ret) => ret[0])', json_encode($name), $relativeImportPath, implode(', ', array_map(fn ($path) => "import($path)", $autoImportPaths))); + } + continue; } $controllerNameForVariable = sprintf('controller_%s', \count($eagerControllerParts)); $importLines[] = sprintf( - "import %s from '%s';", + 'import %s from %s;', $controllerNameForVariable, $relativeImportPath ); + foreach ($autoImportPaths as $autoImportRelativePath) { + $importLines[] = sprintf( + 'import %s;', + $autoImportRelativePath + ); + } $eagerControllerParts[] = sprintf('"%s": %s', $name, $controllerNameForVariable); } diff --git a/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php b/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php index 23d4644ee0c..5a7d5d7352b 100644 --- a/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php +++ b/src/StimulusBundle/src/DependencyInjection/StimulusExtension.php @@ -14,7 +14,7 @@ namespace Symfony\UX\StimulusBundle\DependencyInjection; use Symfony\Component\AssetMapper\AssetMapperInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -40,9 +40,8 @@ public function load(array $configs, ContainerBuilder $container): void ->replaceArgument(2, $config['controller_paths']) ->replaceArgument(3, $config['controllers_json']); - // on older versions, the presence of this service if the trait doesn't exist causes an error - if (!trait_exists(AssetCompilerPathResolverTrait::class)) { - $container->removeDefinition('stimulus.asset_mapper.loader_javascript_compiler'); + if (!class_exists(ImportMapConfigReader::class)) { + $container->removeDefinition('stimulus.asset_mapper.auto_import_locator'); } } diff --git a/src/StimulusBundle/src/Twig/UxControllersTwigRuntime.php b/src/StimulusBundle/src/Twig/UxControllersTwigRuntime.php index 799ff7c7cde..af8cb6023bd 100644 --- a/src/StimulusBundle/src/Twig/UxControllersTwigRuntime.php +++ b/src/StimulusBundle/src/Twig/UxControllersTwigRuntime.php @@ -12,6 +12,7 @@ namespace Symfony\UX\StimulusBundle\Twig; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapGenerator; use Symfony\UX\StimulusBundle\AssetMapper\ControllersMapGenerator; use Symfony\UX\StimulusBundle\Ux\UxPackageReader; use Twig\Extension\RuntimeExtensionInterface; @@ -40,6 +41,12 @@ public function __construct( */ public function renderLinkTags(): string { + if (class_exists(ImportMapGenerator::class)) { + trigger_deprecation('symfony/ux-stimulus-bundle', '2.13.0', 'Calling ux_controller_link_tags() is deprecated and does nothing with symfony/asset-mapper 6.4. The link tags are rendered automatically via the importmap() function.'); + + return ''; + } + $controllersFile = $this->controllersMapGenerator->getControllersJsonPath(); if (!is_file($controllersFile)) { return ''; @@ -66,6 +73,7 @@ public function renderLinkTags(): string return implode("\n", $links); } + // duplicated & adapted in ControllersMapGenerator private function getLinkHref(string $autoImport, string $uxPackageName): string { // see if this is a mapped asset path diff --git a/src/StimulusBundle/src/Ux/UxPackageMetadata.php b/src/StimulusBundle/src/Ux/UxPackageMetadata.php index d78ebf720c7..026edca7ffc 100644 --- a/src/StimulusBundle/src/Ux/UxPackageMetadata.php +++ b/src/StimulusBundle/src/Ux/UxPackageMetadata.php @@ -23,6 +23,9 @@ class UxPackageMetadata public function __construct( public string $packageDirectory, public array $symfonyConfig, + /** + * The package name, without the @. + */ public string $packageName, ) { } diff --git a/src/StimulusBundle/tests/AssetMapper/AutoImportLocatorTest.php b/src/StimulusBundle/tests/AssetMapper/AutoImportLocatorTest.php new file mode 100644 index 00000000000..78a3cc43a4f --- /dev/null +++ b/src/StimulusBundle/tests/AssetMapper/AutoImportLocatorTest.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\UX\StimulusBundle\Tests\AssetMapper; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; +use Symfony\Component\AssetMapper\ImportMap\ImportMapEntry; +use Symfony\Component\AssetMapper\ImportMap\ImportMapType; +use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\UX\StimulusBundle\AssetMapper\AutoImportLocator; +use Symfony\UX\StimulusBundle\Ux\UxPackageMetadata; + +class AutoImportLocatorTest extends TestCase +{ + protected function setUp(): void + { + if (!class_exists(ImportMapConfigReader::class)) { + $this->markTestSkipped('Test requires AssetMapper >= 6.4.'); + } + } + + public function testLocateAutoImportCanHandleAssetMapperPath() + { + $assetMapper = $this->createMock(AssetMapperInterface::class); + $assetMapper->expects($this->once()) + ->method('getAsset') + ->with('foo.css') + ->willReturn(new MappedAsset('foo.css', '/path/to/foo.css')); + + $locator = new AutoImportLocator( + $this->createMock(ImportMapConfigReader::class), + $assetMapper, + ); + $packageMetadata = new UxPackageMetadata('foo', [], 'bar'); + $autoImport = $locator->locateAutoImport('foo.css', $packageMetadata); + $this->assertSame('/path/to/foo.css', $autoImport->path); + $this->assertFalse($autoImport->isBareImport); + } + + public function testLocateAutoImportHandlesFileInPackage() + { + $packageMetadata = new UxPackageMetadata( + __DIR__.'/../fixtures/vendor/fake-vendor/ux-package1/assets', + [], + 'fake-vendor/ux-package1' + ); + + $assetMapper = $this->createMock(AssetMapperInterface::class); + $assetMapper->expects($this->once()) + ->method('getAssetFromSourcePath') + ->with(__DIR__.'/../fixtures/vendor/fake-vendor/ux-package1/assets/dist/styles.css') + ->willReturn(new MappedAsset('styles.css', '/path/to/styles.css')); + + $locator = new AutoImportLocator( + $this->createMock(ImportMapConfigReader::class), + $assetMapper, + ); + + $autoImport = $locator->locateAutoImport('@fake-vendor/ux-package1/dist/styles.css', $packageMetadata); + $this->assertSame('/path/to/styles.css', $autoImport->path); + $this->assertFalse($autoImport->isBareImport); + } + + public function testLocateAutoImportFromImportMap() + { + $importMapConfigReader = $this->createMock(ImportMapConfigReader::class); + $importMapConfigReader->expects($this->once()) + ->method('findRootImportMapEntry') + ->with('tom-select/dist/css/tom-select.default.css') + ->willReturn(ImportMapEntry::createRemote('tom-select/dist/css/tom-select.default.css', ImportMapType::CSS, '/path/to/vendor/tom-select.default.css', '1.0.0', 'tom-select/dist/css/tom-select.default.css', false)); + + $locator = new AutoImportLocator( + $importMapConfigReader, + $this->createMock(AssetMapperInterface::class), + ); + + $packageMetadata = new UxPackageMetadata('foo', [], 'bar'); + $autoImport = $locator->locateAutoImport('tom-select/dist/css/tom-select.default.css', $packageMetadata); + $this->assertSame('tom-select/dist/css/tom-select.default.css', $autoImport->path); + $this->assertTrue($autoImport->isBareImport); + } +} diff --git a/src/StimulusBundle/tests/AssetMapper/ControllerMapGeneratorTest.php b/src/StimulusBundle/tests/AssetMapper/ControllerMapGeneratorTest.php index ea69f01a924..6296be8bd34 100644 --- a/src/StimulusBundle/tests/AssetMapper/ControllerMapGeneratorTest.php +++ b/src/StimulusBundle/tests/AssetMapper/ControllerMapGeneratorTest.php @@ -13,8 +13,11 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\UX\StimulusBundle\AssetMapper\AutoImportLocator; use Symfony\UX\StimulusBundle\AssetMapper\ControllersMapGenerator; +use Symfony\UX\StimulusBundle\AssetMapper\MappedControllerAutoImport; use Symfony\UX\StimulusBundle\Ux\UxPackageReader; class ControllerMapGeneratorTest extends TestCase @@ -43,6 +46,19 @@ public function testGetControllersMap() $packageReader = new UxPackageReader(__DIR__.'/../fixtures'); + $autoImportLocator = $this->createMock(AutoImportLocator::class); + if (class_exists(ImportMapConfigReader::class)) { + $autoImportLocator->expects($this->any()) + ->method('locateAutoImport') + ->willReturnCallback(function ($path) { + return new MappedControllerAutoImport('/path/to'.$path, false); + }); + } else { + // @legacy for AssetMapper 6.3 + $autoImportLocator->expects($this->never()) + ->method('locateAutoImport'); + } + $generator = new ControllersMapGenerator( $mapper, $packageReader, @@ -51,6 +67,7 @@ public function testGetControllersMap() __DIR__.'/../fixtures/assets/more-controllers', ], __DIR__.'/../fixtures/assets/controllers.json', + $autoImportLocator, ); $map = $generator->getControllersMap(); @@ -77,6 +94,11 @@ public function testGetControllersMap() $this->assertSame('fake-vendor/ux-package1/package-controller-second.js', $controllerSecond->asset->logicalPath); // lazy from user's controller.json $this->assertTrue($controllerSecond->isLazy); + // @legacy: assert can be without the conditional for AssetMapper 6.4+ + if (class_exists(ImportMapConfigReader::class)) { + // 4 auto imports from package.json + $this->assertCount(4, $controllerSecond->autoImports); + } $helloControllerFromPackage = $map['fake-vendor--ux-package2--hello-controller']; $this->assertSame('fake-vendor/ux-package2/package-hello-controller.js', $helloControllerFromPackage->asset->logicalPath); diff --git a/src/StimulusBundle/tests/AssetMapper/StimulusControllerLoaderFunctionalTest.php b/src/StimulusBundle/tests/AssetMapper/StimulusControllerLoaderFunctionalTest.php index 8b8f8f099a7..002ea691a59 100644 --- a/src/StimulusBundle/tests/AssetMapper/StimulusControllerLoaderFunctionalTest.php +++ b/src/StimulusBundle/tests/AssetMapper/StimulusControllerLoaderFunctionalTest.php @@ -13,6 +13,7 @@ use Composer\InstalledVersions; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\Filesystem\Filesystem; use Symfony\UX\StimulusBundle\Tests\fixtures\StimulusTestKernel; use Zenstruck\Browser\Test\HasBrowser; @@ -38,46 +39,113 @@ public function testFullApplicationLoad() $importMapJson = $crawler->filter('script[type="importmap"]')->html(); $importMap = json_decode($importMapJson, true); $importMapKeys = array_keys($importMap['imports']); - sort($importMapKeys); - $this->assertSame([ - // 1x import from loader.js (which is aliased to @symfony/stimulus-bundle via importmap) - '/assets/@symfony/stimulus-bundle/controllers.js', - // 6x from "controllers" (hello is overridden) - '/assets/controllers/bye_controller.js', - '/assets/controllers/hello-with-dashes-controller.js', - '/assets/controllers/hello_with_underscores-controller.js', - '/assets/controllers/subdir/deeper-controller.js', - '/assets/controllers/subdir/deeper-with-dashes-controller.js', - '/assets/controllers/subdir/deeper_with_underscores-controller.js', - // 2x from UX packages, which are enabled in controllers.json - '/assets/fake-vendor/ux-package1/package-controller-second.js', - '/assets/fake-vendor/ux-package2/package-hello-controller.js', - // 2x from more-controllers - '/assets/more-controllers/hello-controller.js', - '/assets/more-controllers/other-controller.js', - // 5x from importmap.php - '@hotwired/stimulus', - '@scoped/needed-vendor', - '@symfony/stimulus-bundle', - 'app', - 'needed-vendor', - ], $importMapKeys); - // "app" & loader.js are pre-loaded. So, all non-lazy controllers should be preloaded: - $preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) { - return $link->attr('href'); - }); - $this->assertCount(10, $preLoadHrefs); - sort($preLoadHrefs); - $this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/controllers-', $preLoadHrefs[0]); - $this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/loader-', $preLoadHrefs[1]); - $this->assertStringStartsWith('/assets/controllers/hello-with-dashes-controller-', $preLoadHrefs[2]); - $this->assertStringStartsWith('/assets/controllers/hello_with_underscores-controller-', $preLoadHrefs[3]); - $this->assertStringStartsWith('/assets/controllers/subdir/deeper-controller-', $preLoadHrefs[4]); - $this->assertStringStartsWith('/assets/controllers/subdir/deeper-with-dashes-controller-', $preLoadHrefs[5]); - $this->assertStringStartsWith('/assets/controllers/subdir/deeper_with_underscores-controller-', $preLoadHrefs[6]); - $this->assertStringStartsWith('/assets/fake-vendor/ux-package2/package-hello-controller-', $preLoadHrefs[7]); - $this->assertStringStartsWith('/assets/more-controllers/hello-controller-', $preLoadHrefs[8]); + if (class_exists(ImportMapConfigReader::class)) { + // filter out items ending in .css + $importMapJsKeys = array_filter($importMapKeys, function ($key) { + return '.css' !== substr($key, -4); + }); + $importMapCssKeys = array_filter($importMapKeys, function ($key) { + return '.css' === substr($key, -4); + }); + sort($importMapJsKeys); + $this->assertSame([ + // 1x import from loader.js (which is aliased to @symfony/stimulus-bundle via importmap) + '/assets/@symfony/stimulus-bundle/controllers.js', + // 6x from "controllers" (hello is overridden) + '/assets/controllers/bye_controller.js', + '/assets/controllers/hello-with-dashes-controller.js', + '/assets/controllers/hello_with_underscores-controller.js', + '/assets/controllers/subdir/deeper-controller.js', + '/assets/controllers/subdir/deeper-with-dashes-controller.js', + '/assets/controllers/subdir/deeper_with_underscores-controller.js', + // 2x from UX packages, which are enabled in controllers.json + '/assets/fake-vendor/ux-package1/package-controller-second.js', + '/assets/fake-vendor/ux-package2/package-hello-controller.js', + // 2x from more-controllers + '/assets/more-controllers/hello-controller.js', + '/assets/more-controllers/other-controller.js', + // 5x from importmap.php + '@hotwired/stimulus', + '@scoped/needed-vendor', + '@symfony/stimulus-bundle', + 'app', + 'needed-vendor', + ], array_values($importMapJsKeys)); + + // the autoimport CSS + $this->assertSame([ + '/assets/in/asset/mapper/controller_second1.css', + // enabled => false + // '/assets/in/asset/mapper/controller_second2.css', + '/assets/fake-vendor/ux-package1/styles.css', + + // 2x from importmap.php: so they should, of course, be here. + // But our compiler should not add "path-based" entries + // '/assets/vendor/needed-vendor/file.css', + // '/assets/vendor/@scoped/needed-vendor/the/file2.css', + 'needed-vendor/file.css', + '@scoped/needed-vendor/the/file2.css', + ], array_values($importMapCssKeys)); + + // "app" is the entry. So, all non-lazy controllers should be preloaded: + $preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) { + return $link->attr('href'); + }); + $this->assertCount(11, $preLoadHrefs); + sort($preLoadHrefs); + $this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/controllers-', $preLoadHrefs[0]); + $this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/loader-', $preLoadHrefs[1]); + $this->assertStringStartsWith('/assets/controllers/hello-with-dashes-controller-', $preLoadHrefs[3]); + $this->assertStringStartsWith('/assets/controllers/hello_with_underscores-controller-', $preLoadHrefs[4]); + $this->assertStringStartsWith('/assets/controllers/subdir/deeper-controller-', $preLoadHrefs[5]); + $this->assertStringStartsWith('/assets/controllers/subdir/deeper-with-dashes-controller-', $preLoadHrefs[6]); + $this->assertStringStartsWith('/assets/controllers/subdir/deeper_with_underscores-controller-', $preLoadHrefs[7]); + $this->assertStringStartsWith('/assets/fake-vendor/ux-package2/package-hello-controller-', $preLoadHrefs[8]); + $this->assertStringStartsWith('/assets/more-controllers/hello-controller-', $preLoadHrefs[9]); + $this->assertStringStartsWith('/assets/vendor/@hotwired/stimulus/stimulus.index', $preLoadHrefs[10]); + } else { + // legacy + $this->assertSame([ + // 1x import from loader.js (which is aliased to @symfony/stimulus-bundle via importmap) + '/assets/@symfony/stimulus-bundle/controllers.js', + // 6x from "controllers" (hello is overridden) + '/assets/controllers/bye_controller.js', + '/assets/controllers/hello-with-dashes-controller.js', + '/assets/controllers/hello_with_underscores-controller.js', + '/assets/controllers/subdir/deeper-controller.js', + '/assets/controllers/subdir/deeper-with-dashes-controller.js', + '/assets/controllers/subdir/deeper_with_underscores-controller.js', + // 2x from UX packages, which are enabled in controllers.json + '/assets/fake-vendor/ux-package1/package-controller-second.js', + '/assets/fake-vendor/ux-package2/package-hello-controller.js', + // 2x from more-controllers + '/assets/more-controllers/hello-controller.js', + '/assets/more-controllers/other-controller.js', + // 5x from importmap.php + '@hotwired/stimulus', + '@scoped/needed-vendor', + '@symfony/stimulus-bundle', + 'app', + 'needed-vendor', + ], $importMapKeys); + + // "app" & loader.js are pre-loaded. So, all non-lazy controllers should be preloaded: + $preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) { + return $link->attr('href'); + }); + $this->assertCount(10, $preLoadHrefs); + sort($preLoadHrefs); + $this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/controllers-', $preLoadHrefs[0]); + $this->assertStringStartsWith('/assets/@symfony/stimulus-bundle/loader-', $preLoadHrefs[1]); + $this->assertStringStartsWith('/assets/controllers/hello-with-dashes-controller-', $preLoadHrefs[2]); + $this->assertStringStartsWith('/assets/controllers/hello_with_underscores-controller-', $preLoadHrefs[3]); + $this->assertStringStartsWith('/assets/controllers/subdir/deeper-controller-', $preLoadHrefs[4]); + $this->assertStringStartsWith('/assets/controllers/subdir/deeper-with-dashes-controller-', $preLoadHrefs[5]); + $this->assertStringStartsWith('/assets/controllers/subdir/deeper_with_underscores-controller-', $preLoadHrefs[6]); + $this->assertStringStartsWith('/assets/fake-vendor/ux-package2/package-hello-controller-', $preLoadHrefs[7]); + $this->assertStringStartsWith('/assets/more-controllers/hello-controller-', $preLoadHrefs[8]); + } } protected static function getKernelClass(): string diff --git a/src/StimulusBundle/tests/AssetMapper/StimulusLoaderJavaScriptCompilerTest.php b/src/StimulusBundle/tests/AssetMapper/StimulusLoaderJavaScriptCompilerTest.php index 405c257ad40..308e59413b3 100644 --- a/src/StimulusBundle/tests/AssetMapper/StimulusLoaderJavaScriptCompilerTest.php +++ b/src/StimulusBundle/tests/AssetMapper/StimulusLoaderJavaScriptCompilerTest.php @@ -53,11 +53,11 @@ public function testCompileDynamicallyAddsContents() $compiledContents = $compiler->compile($startingContents, $loaderAsset, $this->createMock(AssetMapperInterface::class)); $this->assertStringContainsString( - "import controller_0 from '../../controllers/foo-controller.js';", + 'import controller_0 from "../../controllers/foo-controller.js";', $compiledContents, ); $this->assertStringContainsString( - "import controller_1 from '../../in-root_controller.js';", + 'import controller_1 from "../../in-root_controller.js";', $compiledContents, ); $this->assertStringContainsString( @@ -107,7 +107,7 @@ public function testDebugModeIsSetCorrectly() private function createAsset(string $publicPath): MappedAsset { - $asset = new MappedAsset(basename($publicPath), publicPathWithoutDigest: $publicPath); + $asset = new MappedAsset(basename($publicPath), '/path/to/project/'.$publicPath, publicPathWithoutDigest: $publicPath); return $asset; } diff --git a/src/StimulusBundle/tests/Twig/UxControllersTwigRuntimeTest.php b/src/StimulusBundle/tests/Twig/UxControllersTwigRuntimeTest.php index 2eeb7c93f69..fd507ae05b8 100644 --- a/src/StimulusBundle/tests/Twig/UxControllersTwigRuntimeTest.php +++ b/src/StimulusBundle/tests/Twig/UxControllersTwigRuntimeTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\AssetMapper\AssetMapperInterface; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\AssetMapper\MappedAsset; use Symfony\UX\StimulusBundle\AssetMapper\ControllersMapGenerator; use Symfony\UX\StimulusBundle\Twig\UxControllersTwigRuntime; @@ -20,8 +21,15 @@ class UxControllersTwigRuntimeTest extends TestCase { + /** + * @group legacy + */ public function testRenderLinkTags() { + if (class_exists(ImportMapConfigReader::class)) { + $this->markTestSkipped('Skip test for AssetMapper 6.4+'); + } + $controllersMapGenerator = $this->createMock(ControllersMapGenerator::class); $controllersMapGenerator->expects($this->any()) ->method('getControllersJsonPath') @@ -47,7 +55,7 @@ public function testRenderLinkTags() $controllersMapGenerator, $assetMapper, new UxPackageReader(__DIR__.'/../fixtures'), - __DIR__.'/../fixtures' + __DIR__.'/../fixtures/legacy' ); $this->assertStringNotContainsString( diff --git a/src/StimulusBundle/tests/fixtures/StimulusTestKernel.php b/src/StimulusBundle/tests/fixtures/StimulusTestKernel.php index 3130358a0c4..fea75c00201 100644 --- a/src/StimulusBundle/tests/fixtures/StimulusTestKernel.php +++ b/src/StimulusBundle/tests/fixtures/StimulusTestKernel.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\HttpFoundation\Response; @@ -54,9 +55,12 @@ protected function configureContainer(ContainerConfigurator $container): void 'asset_mapper' => [ 'paths' => [ 'assets' => '', + 'in-asset-mapper' => 'in/asset/mapper', __DIR__.'/vendor/fake-vendor/ux-package1/assets/dist' => 'fake-vendor/ux-package1', __DIR__.'/vendor/fake-vendor/ux-package2/Resources/assets/dist' => 'fake-vendor/ux-package2', ], + // @legacy + 'importmap_path' => '%kernel.project_dir%/'.(class_exists(ImportMapConfigReader::class) ? 'importmap.php' : 'legacy/importmap.php'), ], 'test' => true, 'handle_all_throwables' => true, diff --git a/src/StimulusBundle/tests/fixtures/assets/controllers.json b/src/StimulusBundle/tests/fixtures/assets/controllers.json index 11d7a50bd25..c6fb358e2d4 100644 --- a/src/StimulusBundle/tests/fixtures/assets/controllers.json +++ b/src/StimulusBundle/tests/fixtures/assets/controllers.json @@ -13,7 +13,7 @@ "fetch": "lazy", "autoimport": { "in/asset/mapper/controller_second1.css": true, - "in/asset/mapper.controller_second2.css": false, + "in/asset/mapper/controller_second2.css": false, "@fake-vendor/ux-package1/dist/styles.css": true, "needed-vendor/file.css": true, "@scoped/needed-vendor/the/file2.css": true diff --git a/src/StimulusBundle/tests/fixtures/assets/vendor/@hotwired/stimulus/stimulus.index.js b/src/StimulusBundle/tests/fixtures/assets/vendor/@hotwired/stimulus/stimulus.index.js new file mode 100644 index 00000000000..b56503e1f38 --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/assets/vendor/@hotwired/stimulus/stimulus.index.js @@ -0,0 +1 @@ +console.log('stimulus.index.js'); diff --git a/src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/needed-vendor.index.js b/src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/needed-vendor.index.js new file mode 100644 index 00000000000..837675e1dc5 --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/needed-vendor.index.js @@ -0,0 +1 @@ +console.log('scoped needed-vendor.index.js'); diff --git a/src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/the/file2.css b/src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/the/file2.css new file mode 100644 index 00000000000..1f56267d17f --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/assets/vendor/@scoped/needed-vendor/the/file2.css @@ -0,0 +1 @@ +/* file2.css */ diff --git a/src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/file.css b/src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/file.css new file mode 100644 index 00000000000..a45e860d0d4 --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/file.css @@ -0,0 +1 @@ +/* file.css */ diff --git a/src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/needed-vendor.index.js b/src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/needed-vendor.index.js new file mode 100644 index 00000000000..0bed9f2b0f9 --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/assets/vendor/needed-vendor/needed-vendor.index.js @@ -0,0 +1 @@ +console.log('needed-vendor.index.js'); diff --git a/src/StimulusBundle/tests/fixtures/importmap.php b/src/StimulusBundle/tests/fixtures/importmap.php index 24c25c5b247..b33bbd17723 100644 --- a/src/StimulusBundle/tests/fixtures/importmap.php +++ b/src/StimulusBundle/tests/fixtures/importmap.php @@ -15,15 +15,23 @@ 'entrypoint' => true, ], '@hotwired/stimulus' => [ - 'url' => 'https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js', + 'version' => '3.2.1', ], '@symfony/stimulus-bundle' => [ 'path' => '@symfony/stimulus-bundle/loader.js', ], 'needed-vendor' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/needed-vendor@3.2.0/dist/needed-vendor+esm', + 'version' => '3.2.0', + ], + 'needed-vendor/file.css' => [ + 'version' => '3.2.0', + 'type' => 'css', ], '@scoped/needed-vendor' => [ - 'url' => 'https://cdn.jsdelivr.net/npm/@scoped/needed-vendor@1.2.3', + 'version' => '1.2.3', + ], + '@scoped/needed-vendor/the/file2.css' => [ + 'version' => '1.2.3', + 'type' => 'css', ], ]; diff --git a/src/StimulusBundle/tests/fixtures/in-asset-mapper/controller_second1.css b/src/StimulusBundle/tests/fixtures/in-asset-mapper/controller_second1.css new file mode 100644 index 00000000000..85e8d949d29 --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/in-asset-mapper/controller_second1.css @@ -0,0 +1 @@ +/* controller_second1.css */ diff --git a/src/StimulusBundle/tests/fixtures/legacy/importmap.php b/src/StimulusBundle/tests/fixtures/legacy/importmap.php new file mode 100644 index 00000000000..998ee44649c --- /dev/null +++ b/src/StimulusBundle/tests/fixtures/legacy/importmap.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// @legacy for AssetMapper 6.3 support +return [ + 'app' => [ + 'path' => 'app.js', + 'entrypoint' => true, + ], + '@hotwired/stimulus' => [ + 'url' => 'https://ga.jspm.io/npm:@hotwired/stimulus@3.2.1/dist/stimulus.js', + ], + '@symfony/stimulus-bundle' => [ + 'path' => '@symfony/stimulus-bundle/loader.js', + ], + 'needed-vendor' => [ + 'url' => 'https://cdn.jsdelivr.net/npm/needed-vendor@3.2.0/dist/needed-vendor+esm', + ], + '@scoped/needed-vendor' => [ + 'url' => 'https://cdn.jsdelivr.net/npm/@scoped/needed-vendor@1.2.3', + ], +]; diff --git a/src/StimulusBundle/tests/fixtures/vendor/fake-vendor/ux-package1/assets/dist/styles.css b/src/StimulusBundle/tests/fixtures/vendor/fake-vendor/ux-package1/assets/dist/styles.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Svelte/src/AssetMapper/SvelteControllerLoaderAssetCompiler.php b/src/Svelte/src/AssetMapper/SvelteControllerLoaderAssetCompiler.php index a27672e9511..2e36fb6d003 100644 --- a/src/Svelte/src/AssetMapper/SvelteControllerLoaderAssetCompiler.php +++ b/src/Svelte/src/AssetMapper/SvelteControllerLoaderAssetCompiler.php @@ -11,10 +11,11 @@ namespace Symfony\UX\Svelte\AssetMapper; +use Symfony\Component\AssetMapper\AssetDependency; use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; /** @@ -24,8 +25,6 @@ */ class SvelteControllerLoaderAssetCompiler implements AssetCompilerInterface { - use AssetCompilerPathResolverTrait; - public function __construct( private string $controllerPath, private array $nameGlobs, @@ -41,10 +40,15 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac { $importLines = []; $componentParts = []; - $loaderPublicPath = $asset->publicPathWithoutDigest; foreach ($this->findControllerAssets($assetMapper) as $name => $mappedAsset) { - $controllerPublicPath = $mappedAsset->publicPathWithoutDigest; - $relativeImportPath = $this->createRelativePath($loaderPublicPath, $controllerPublicPath); + // @legacy: backwards compatibility with Symfony 6.3 + if (class_exists(AssetDependency::class)) { + $controllerPublicPath = $mappedAsset->publicPathWithoutDigest; + $loaderPublicPath = $asset->publicPathWithoutDigest; + $relativeImportPath = Path::makeRelative($controllerPublicPath, \dirname($loaderPublicPath)); + } else { + $relativeImportPath = Path::makeRelative($mappedAsset->sourcePath, \dirname($asset->sourcePath)); + } $controllerNameForVariable = sprintf('component_%s', \count($componentParts)); diff --git a/src/Svelte/src/DependencyInjection/SvelteExtension.php b/src/Svelte/src/DependencyInjection/SvelteExtension.php index ea97c4e9c5d..1b4af03dead 100644 --- a/src/Svelte/src/DependencyInjection/SvelteExtension.php +++ b/src/Svelte/src/DependencyInjection/SvelteExtension.php @@ -12,7 +12,6 @@ namespace Symfony\UX\Svelte\DependencyInjection; use Symfony\Component\AssetMapper\AssetMapperInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -44,16 +43,13 @@ public function load(array $configs, ContainerBuilder $container) ->setPublic(false) ; - // on older versions, the absence of this trait will trigger an error if the service is loaded - if (trait_exists(AssetCompilerPathResolverTrait::class)) { - $container->setDefinition('svelte.asset_mapper.svelte_controller_loader_compiler', new Definition(SvelteControllerLoaderAssetCompiler::class)) - ->setArguments([ - $config['controllers_path'], - $config['name_glob'], - ]) - // run before the core JavaScript compiler - ->addTag('asset_mapper.compiler', ['priority' => 100]); - } + $container->setDefinition('svelte.asset_mapper.svelte_controller_loader_compiler', new Definition(SvelteControllerLoaderAssetCompiler::class)) + ->setArguments([ + $config['controllers_path'], + $config['name_glob'], + ]) + // run before the core JavaScript compiler + ->addTag('asset_mapper.compiler', ['priority' => 100]); } public function prepend(ContainerBuilder $container) diff --git a/src/Svelte/tests/AssetMapper/SvelteControllerLoaderAssetCompilerTest.php b/src/Svelte/tests/AssetMapper/SvelteControllerLoaderAssetCompilerTest.php index a66b43cc170..1faf1ebcdb6 100644 --- a/src/Svelte/tests/AssetMapper/SvelteControllerLoaderAssetCompilerTest.php +++ b/src/Svelte/tests/AssetMapper/SvelteControllerLoaderAssetCompilerTest.php @@ -31,6 +31,7 @@ public function testCompileDynamicallyAddsContents() if (str_contains($sourcePath, 'MySvelteController')) { return new MappedAsset( 'MySvelteController.js', + '/project/assets/svelte/controllers/MySvelteController.js', publicPathWithoutDigest: '/assets/svelte/controllers/MySvelteController.js', ); } @@ -38,6 +39,7 @@ public function testCompileDynamicallyAddsContents() if (str_contains($sourcePath, 'DeeperSvelteController')) { return new MappedAsset( 'subdir/DeeperSvelteController.js', + '/project/assets/svelte/controllers/subdir/DeeperSvelteController.js', publicPathWithoutDigest: '/assets/svelte/controllers/subdir/DeeperSvelteController.js', ); } @@ -50,7 +52,7 @@ public function testCompileDynamicallyAddsContents() ['*.js'] ); - $loaderAsset = new MappedAsset('loader.js', publicPathWithoutDigest: '/assets/symfony/ux-svelte/loader.js'); + $loaderAsset = new MappedAsset('loader.js', '/project/assets/vendor/StimulusBundle/loader.js', publicPathWithoutDigest: '/assets/symfony/ux-svelte/loader.js'); $startingContents = file_get_contents(__DIR__.'/../../assets/dist/loader.js'); $compiledContents = $compiler->compile($startingContents, $loaderAsset, $assetMapper); diff --git a/src/TwigComponent/tests/Fixtures/Kernel.php b/src/TwigComponent/tests/Fixtures/Kernel.php index 7fc8895d18a..934a3d923b5 100644 --- a/src/TwigComponent/tests/Fixtures/Kernel.php +++ b/src/TwigComponent/tests/Fixtures/Kernel.php @@ -11,6 +11,7 @@ namespace Symfony\UX\TwigComponent\Tests\Fixtures; +use Psr\Log\NullLogger; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -80,6 +81,7 @@ protected function configureContainer(ContainerConfigurator $c): void ->autowire() ->autoconfigure() ->load(__NAMESPACE__.'\\', __DIR__) + ->set('logger', NullLogger::class) ->set('component_b', ComponentB::class)->autoconfigure()->autowire() ->set('component_d', ComponentB::class)->tag('twig.component', [ 'key' => 'component_d', diff --git a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php index 1fd8a961ac2..07eb7397ab7 100644 --- a/src/TwigComponent/tests/Integration/ComponentExtensionTest.php +++ b/src/TwigComponent/tests/Integration/ComponentExtensionTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\UX\TwigComponent\Tests\Fixtures\User; use Twig\Environment; +use Twig\Error\RuntimeError; /** * @author Kevin Bond @@ -62,7 +63,12 @@ public function testCanRenderComponentWithMoreAdvancedTwigExpressions(): void public function testCanNotRenderComponentWithInvalidExpressions(): void { - $this->expectException(\TypeError::class); + if (Environment::MAJOR_VERSION === 2) { + $this->expectException(\TypeError::class); + } else { + $this->expectException(RuntimeError::class); + } + self::getContainer()->get(Environment::class)->render('invalid_flexible_component.html.twig'); } diff --git a/src/Vue/src/AssetMapper/VueControllerLoaderAssetCompiler.php b/src/Vue/src/AssetMapper/VueControllerLoaderAssetCompiler.php index d50d54a2e38..b0f4db157a3 100644 --- a/src/Vue/src/AssetMapper/VueControllerLoaderAssetCompiler.php +++ b/src/Vue/src/AssetMapper/VueControllerLoaderAssetCompiler.php @@ -11,10 +11,11 @@ namespace Symfony\UX\Vue\AssetMapper; +use Symfony\Component\AssetMapper\AssetDependency; use Symfony\Component\AssetMapper\AssetMapperInterface; use Symfony\Component\AssetMapper\Compiler\AssetCompilerInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\AssetMapper\MappedAsset; +use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Finder; /** @@ -24,8 +25,6 @@ */ class VueControllerLoaderAssetCompiler implements AssetCompilerInterface { - use AssetCompilerPathResolverTrait; - public function __construct( private string $controllerPath, private array $nameGlobs, @@ -41,10 +40,15 @@ public function compile(string $content, MappedAsset $asset, AssetMapperInterfac { $importLines = []; $componentParts = []; - $loaderPublicPath = $asset->publicPathWithoutDigest; foreach ($this->findControllerAssets($assetMapper) as $name => $mappedAsset) { - $controllerPublicPath = $mappedAsset->publicPathWithoutDigest; - $relativeImportPath = $this->createRelativePath($loaderPublicPath, $controllerPublicPath); + // @legacy: backwards compatibility with Symfony 6.3 + if (class_exists(AssetDependency::class)) { + $controllerPublicPath = $mappedAsset->publicPathWithoutDigest; + $loaderPublicPath = $asset->publicPathWithoutDigest; + $relativeImportPath = Path::makeRelative($controllerPublicPath, \dirname($loaderPublicPath)); + } else { + $relativeImportPath = Path::makeRelative($mappedAsset->sourcePath, \dirname($asset->sourcePath)); + } $controllerNameForVariable = sprintf('component_%s', \count($componentParts)); diff --git a/src/Vue/src/DependencyInjection/VueExtension.php b/src/Vue/src/DependencyInjection/VueExtension.php index 9bb3b527797..f340b172c06 100644 --- a/src/Vue/src/DependencyInjection/VueExtension.php +++ b/src/Vue/src/DependencyInjection/VueExtension.php @@ -12,7 +12,6 @@ namespace Symfony\UX\Vue\DependencyInjection; use Symfony\Component\AssetMapper\AssetMapperInterface; -use Symfony\Component\AssetMapper\Compiler\AssetCompilerPathResolverTrait; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -44,17 +43,14 @@ public function load(array $configs, ContainerBuilder $container) ->setPublic(false) ; - // on older versions, the absence of this trait will trigger an error if the service is loaded - if (trait_exists(AssetCompilerPathResolverTrait::class)) { - $container->setDefinition('vue.asset_mapper.vue_controller_loader_compiler', new Definition(VueControllerLoaderAssetCompiler::class)) - ->setArguments([ - $config['controllers_path'], - $config['name_glob'], - ]) - // run before the core JavaScript compiler - ->addTag('asset_mapper.compiler', ['priority' => 100]) - ; - } + $container->setDefinition('vue.asset_mapper.vue_controller_loader_compiler', new Definition(VueControllerLoaderAssetCompiler::class)) + ->setArguments([ + $config['controllers_path'], + $config['name_glob'], + ]) + // run before the core JavaScript compiler + ->addTag('asset_mapper.compiler', ['priority' => 100]) + ; } public function prepend(ContainerBuilder $container) diff --git a/src/Vue/tests/AssetMapper/VueControllerLoaderAssetCompilerTest.php b/src/Vue/tests/AssetMapper/VueControllerLoaderAssetCompilerTest.php index 8ed85a218d1..9fceef0da00 100644 --- a/src/Vue/tests/AssetMapper/VueControllerLoaderAssetCompilerTest.php +++ b/src/Vue/tests/AssetMapper/VueControllerLoaderAssetCompilerTest.php @@ -31,6 +31,7 @@ public function testCompileDynamicallyAddsContents() if (str_contains($sourcePath, 'MyVueController')) { return new MappedAsset( 'MyVueController.js', + '/project/assets/vue/controllers/MyVueController.js', publicPathWithoutDigest: '/assets/vue/controllers/MyVueController.js', ); } @@ -38,6 +39,7 @@ public function testCompileDynamicallyAddsContents() if (str_contains($sourcePath, 'DeeperVueController')) { return new MappedAsset( 'subdir/DeeperVueController.js', + '/project/assets/vue/controllers/subdir/DeeperVueController.js', publicPathWithoutDigest: '/assets/vue/controllers/subdir/DeeperVueController.js', ); } @@ -50,7 +52,7 @@ public function testCompileDynamicallyAddsContents() ['*.js'] ); - $loaderAsset = new MappedAsset('loader.js', publicPathWithoutDigest: '/assets/symfony/ux-vue/loader.js'); + $loaderAsset = new MappedAsset('loader.js', '/project/assets/vendor/StimulusBundle/loader.js', publicPathWithoutDigest: '/assets/symfony/ux-vue/loader.js'); $startingContents = file_get_contents(__DIR__.'/../../assets/dist/loader.js'); $compiledContents = $compiler->compile($startingContents, $loaderAsset, $assetMapper);