mirror of
https://github.com/symfony/ux-svelte.git
synced 2026-03-24 00:12:07 +01:00
Asset mapper integration part 2!
This commit is contained in:
5
assets/dist/components.d.ts
vendored
Normal file
5
assets/dist/components.d.ts
vendored
Normal 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
3
assets/dist/components.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
const components = {};
|
||||
|
||||
export { components };
|
||||
9
assets/dist/loader.d.ts
vendored
Normal file
9
assets/dist/loader.d.ts
vendored
Normal 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
14
assets/dist/loader.js
vendored
Normal 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 };
|
||||
@@ -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
17
assets/src/components.ts
Normal 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
36
assets/src/loader.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
96
src/AssetMapper/SvelteControllerLoaderAssetCompiler.php
Normal file
96
src/AssetMapper/SvelteControllerLoaderAssetCompiler.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
1
tests/fixtures/svelte/components/MySvelteComponent.js
vendored
Normal file
1
tests/fixtures/svelte/components/MySvelteComponent.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('MySvelteComponent.js')
|
||||
1
tests/fixtures/svelte/controllers/MySvelteController.js
vendored
Normal file
1
tests/fixtures/svelte/controllers/MySvelteController.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('MySvelteController.js')
|
||||
1
tests/fixtures/svelte/controllers/other-file.txt
vendored
Normal file
1
tests/fixtures/svelte/controllers/other-file.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Other file - not a Svelte file.
|
||||
1
tests/fixtures/svelte/controllers/subdir/DeeperSvelteController.js
vendored
Normal file
1
tests/fixtures/svelte/controllers/subdir/DeeperSvelteController.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
console.log('DeeperSvelteController.js')
|
||||
Reference in New Issue
Block a user