Asset mapper integration part 2!

This commit is contained in:
Ryan Weaver
2023-05-22 12:50:49 -04:00
parent a85e08ad00
commit 929601af61
16 changed files with 306 additions and 3 deletions

5
assets/dist/components.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import type { SvelteComponent } from 'svelte';
export interface ComponentCollection {
[key: string]: SvelteComponent;
}
export declare const components: ComponentCollection;

3
assets/dist/components.js vendored Normal file
View File

@@ -0,0 +1,3 @@
const components = {};
export { components };

9
assets/dist/loader.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import type { SvelteComponent } from 'svelte';
import { ComponentCollection } from './components.js';
declare global {
function resolveSvelteComponent(name: string): typeof SvelteComponent;
interface Window {
resolveSvelteComponent(name: string): typeof SvelteComponent;
}
}
export declare function registerSvelteControllerComponents(svelteComponents?: ComponentCollection): void;

14
assets/dist/loader.js vendored Normal file
View File

@@ -0,0 +1,14 @@
import { components } from './components.js';
function registerSvelteControllerComponents(svelteComponents = components) {
window.resolveSvelteComponent = (name) => {
const component = svelteComponents[name];
if (typeof component === 'undefined') {
const possibleValues = Object.keys(svelteComponents).length > 0 ? Object.keys(svelteComponents).join(', ') : 'none';
throw new Error(`Svelte controller "${name}" does not exist. Possible values: ${possibleValues}`);
}
return component;
};
}
export { registerSvelteControllerComponents };

View File

@@ -15,7 +15,8 @@
},
"importmap": {
"@hotwired/stimulus": "^3.0.0",
"svelte": "^3.0"
"svelte/internal": "^3.0",
"@symfony/ux-svelte": "path:dist/loader.js"
}
},
"peerDependencies": {

17
assets/src/components.ts Normal file
View File

@@ -0,0 +1,17 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
// This file is dynamically rewritten by ux-svelte + AssetMapper.
import type { SvelteComponent } from 'svelte';
export interface ComponentCollection {
[key: string]: SvelteComponent;
}
export const components: ComponentCollection = {};

36
assets/src/loader.ts Normal file
View File

@@ -0,0 +1,36 @@
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
'use strict';
import type { SvelteComponent } from 'svelte';
import { ComponentCollection, components } from './components.js';
declare global {
function resolveSvelteComponent(name: string): typeof SvelteComponent;
interface Window {
resolveSvelteComponent(name: string): typeof SvelteComponent;
}
}
export function registerSvelteControllerComponents(svelteComponents: ComponentCollection = components): void {
// Expose a global Svelte loader to allow rendering from the Stimulus controller
(window as any).resolveSvelteComponent = (name: string): SvelteComponent => {
const component = svelteComponents[name];
if (typeof component === 'undefined') {
const possibleValues: string =
Object.keys(svelteComponents).length > 0 ? Object.keys(svelteComponents).join(', ') : 'none';
throw new Error(`Svelte controller "${name}" does not exist. Possible values: ${possibleValues}`);
}
return component;
};
}

View File

@@ -36,6 +36,8 @@
"symfony/stimulus-bundle": "^2.9"
},
"require-dev": {
"symfony/asset-mapper": "6.3.x-dev",
"symfony/finder": "^5.4|^6.2",
"symfony/framework-bundle": "^5.4|^6.2",
"symfony/phpunit-bridge": "^5.4|^6.2",
"symfony/twig-bundle": "^5.4|^6.2",

View File

@@ -0,0 +1,96 @@
<?php
/*
* This file is part of the Symfony StimulusBundle package.
* (c) Fabien Potencier <fabien@symfony.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Svelte\AssetMapper;
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\Finder\Finder;
/**
* Compiles the components.js file to dynamically import the Svelte controller components.
*
* @author Ryan Weaver <ryan@symfonycasts.com>
*/
class SvelteControllerLoaderAssetCompiler implements AssetCompilerInterface
{
use AssetCompilerPathResolverTrait;
public function __construct(
private string $controllerPath,
private array $nameGlobs,
) {
}
public function supports(MappedAsset $asset): bool
{
return $asset->sourcePath === realpath(__DIR__.'/../../assets/dist/components.js');
}
public function compile(string $content, MappedAsset $asset, AssetMapperInterface $assetMapper): string
{
$importLines = [];
$componentParts = [];
$loaderPublicPath = $asset->publicPathWithoutDigest;
foreach ($this->findControllerAssets($assetMapper) as $name => $mappedAsset) {
$controllerPublicPath = $mappedAsset->publicPathWithoutDigest;
$relativeImportPath = $this->createRelativePath($loaderPublicPath, $controllerPublicPath);
$controllerNameForVariable = sprintf('component_%s', \count($componentParts));
$importLines[] = sprintf(
"import %s from '%s';",
$controllerNameForVariable,
$relativeImportPath
);
$componentParts[] = sprintf('"%s": %s', $name, $controllerNameForVariable);
}
$importCode = implode("\n", $importLines);
$componentsJson = sprintf('{%s}', implode(', ', $componentParts));
return <<<EOF
$importCode
export const components = $componentsJson;
EOF;
}
/**
* @return MappedAsset[]
*/
private function findControllerAssets(AssetMapperInterface $assetMapper): array
{
if (!class_exists(Finder::class)) {
throw new \LogicException('The "symfony/finder" package is required to use ux-Svelte with AssetMapper. Try running "composer require symfony/finder".');
}
$finder = new Finder();
$finder->in($this->controllerPath)
->files()
->name($this->nameGlobs)
;
$assets = [];
foreach ($finder as $file) {
$asset = $assetMapper->getAssetFromSourcePath($file->getRealPath());
if (null === $asset) {
throw new \LogicException(sprintf('Could not find an asset mapper path for the Svelte controller file "%s".', $file->getRealPath()));
}
$name = $file->getRelativePathname();
$name = substr($name, 0, -\strlen($file->getExtension()) - 1);
$assets[$name] = $asset;
}
return $assets;
}
}

View File

@@ -12,11 +12,15 @@
namespace Symfony\UX\Svelte\DependencyInjection;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\UX\Svelte\AssetMapper\SvelteControllerLoaderAssetCompiler;
use Symfony\UX\Svelte\Twig\SvelteComponentExtension;
/**
@@ -25,16 +29,28 @@ use Symfony\UX\Svelte\Twig\SvelteComponentExtension;
*
* @internal
*/
class SvelteExtension extends Extension implements PrependExtensionInterface
class SvelteExtension extends Extension implements PrependExtensionInterface, ConfigurationInterface
{
public function load(array $configs, ContainerBuilder $container)
{
$configuration = $this->getConfiguration($configs, $container);
$config = $this->processConfiguration($configuration, $configs);
$container
->setDefinition('twig.extension.svelte', new Definition(SvelteComponentExtension::class))
->setArgument(0, new Reference('stimulus.helper'))
->addTag('twig.extension')
->setPublic(false)
;
$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)
@@ -51,4 +67,32 @@ class SvelteExtension extends Extension implements PrependExtensionInterface
],
]);
}
public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface
{
return $this;
}
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('svelte');
$rootNode = $treeBuilder->getRootNode();
\assert($rootNode instanceof ArrayNodeDefinition);
$rootNode
->children()
->scalarNode('controllers_path')
->info('The path to the directory where Svelte controller components are stored - relevant only when using symfony/asset-mapper.')
->defaultValue('%kernel.project_dir%/assets/svelte/controllers')
->end()
->arrayNode('name_glob')
->info('The glob patterns to use to find Svelte controller components inside of controllers_path')
// find .js (already compiled) or .svelte, in case the user will have an asset compiler to do the .svelte -> .js compilation
->defaultValue(['*.js', '*.svelte'])
->scalarPrototype()->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony StimulusBundle package.
* (c) Fabien Potencier <fabien@symfony.com>
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\UX\Svelte\Tests\AssetMapper;
use PHPUnit\Framework\TestCase;
use Symfony\Component\AssetMapper\AssetMapperInterface;
use Symfony\Component\AssetMapper\MappedAsset;
use Symfony\UX\Svelte\AssetMapper\SvelteControllerLoaderAssetCompiler;
class SvelteControllerLoaderAssetCompilerTest extends TestCase
{
public function testCompileDynamicallyAddsContents()
{
$assetMapper = $this->createMock(AssetMapperInterface::class);
$assetMapper->expects($this->exactly(2))
->method('getAssetFromSourcePath')
->with($this->logicalOr(
$this->equalTo(realpath(__DIR__.'/../fixtures/svelte/controllers/MySvelteController.js')),
$this->equalTo(realpath(__DIR__.'/../fixtures/svelte/controllers/subdir/DeeperSvelteController.js')),
))
->willReturnCallback(function ($sourcePath) {
if (str_contains($sourcePath, 'MySvelteController')) {
return new MappedAsset(
'MySvelteController.js',
publicPathWithoutDigest: '/assets/svelte/controllers/MySvelteController.js',
);
}
if (str_contains($sourcePath, 'DeeperSvelteController')) {
return new MappedAsset(
'subdir/DeeperSvelteController.js',
publicPathWithoutDigest: '/assets/svelte/controllers/subdir/DeeperSvelteController.js',
);
}
throw new \Exception('Unexpected source path: '.$sourcePath);
});
$compiler = new SvelteControllerLoaderAssetCompiler(
__DIR__.'/../fixtures/svelte/controllers',
['*.js']
);
$loaderAsset = new MappedAsset('loader.js', publicPathWithoutDigest: '/assets/symfony/ux-svelte/loader.js');
$startingContents = file_get_contents(__DIR__.'/../../assets/dist/loader.js');
$compiledContents = $compiler->compile($startingContents, $loaderAsset, $assetMapper);
$this->assertStringContainsString(
"from '../../svelte/controllers/subdir/DeeperSvelteController.js';",
$compiledContents,
);
$this->assertStringContainsString(
"from '../../svelte/controllers/MySvelteController.js';",
$compiledContents,
);
$this->assertStringContainsString(
'export const components = {"',
$compiledContents,
);
$this->assertStringContainsString(
'"subdir/DeeperSvelteController": component_',
$compiledContents,
);
}
}

View File

@@ -35,7 +35,7 @@ class TwigAppKernel extends Kernel
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load(function (ContainerBuilder $container) {
$container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true]);
$container->loadFromExtension('framework', ['secret' => '$ecret', 'test' => true, 'http_method_override' => false]);
$container->loadFromExtension('twig', ['default_path' => __DIR__.'/templates', 'strict_variables' => true, 'exception_controller' => null]);
$container->setAlias('test.twig', 'twig')->setPublic(true);

View File

@@ -0,0 +1 @@
console.log('MySvelteComponent.js')

View File

@@ -0,0 +1 @@
console.log('MySvelteController.js')

View File

@@ -0,0 +1 @@
Other file - not a Svelte file.

View File

@@ -0,0 +1 @@
console.log('DeeperSvelteController.js')