Skip to content

Commit c693b65

Browse files
committed
[Svelte] Svelte 5 support
1 parent 9ad36a4 commit c693b65

12 files changed

+232
-140
lines changed

src/Svelte/assets/dist/render_controller.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ export default class extends Controller<Element & {
1414
props: ObjectConstructor;
1515
intro: BooleanConstructor;
1616
};
17-
connect(): void;
17+
connect(): Promise<void>;
1818
disconnect(): void;
1919
_destroyIfExists(): void;
2020
private dispatchEvent;
21+
private mountSvelteComponent;
2122
}

src/Svelte/assets/dist/render_controller.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { Controller } from '@hotwired/stimulus';
2+
import { VERSION } from 'svelte/compiler';
23

34
class default_1 extends Controller {
4-
connect() {
5+
async connect() {
56
this.element.innerHTML = '';
67
this.props = this.propsValue ?? undefined;
78
this.intro = this.introValue ?? undefined;
89
this.dispatchEvent('connect');
910
const Component = window.resolveSvelteComponent(this.componentValue);
1011
this._destroyIfExists();
11-
this.app = new Component({
12+
this.app = await this.mountSvelteComponent(Component, {
1213
target: this.element,
1314
props: this.props,
1415
intro: this.intro,
@@ -37,6 +38,13 @@ class default_1 extends Controller {
3738
};
3839
this.dispatch(name, { detail, prefix: 'svelte' });
3940
}
41+
async mountSvelteComponent(Component, options) {
42+
if (VERSION?.startsWith('5')) {
43+
const { mount } = await import('svelte');
44+
return mount(Component, options);
45+
}
46+
return new Component(options);
47+
}
4048
}
4149
default_1.values = {
4250
component: String,

src/Svelte/assets/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
},
2121
"peerDependencies": {
2222
"@hotwired/stimulus": "^3.0.0",
23-
"svelte": "^3.0 || ^4.0"
23+
"svelte": "^3.0 || ^4.0 || ^5.0"
2424
},
2525
"devDependencies": {
2626
"@hotwired/stimulus": "^3.0.0",
27-
"@sveltejs/vite-plugin-svelte": "^2.4.6",
27+
"@sveltejs/vite-plugin-svelte": "^3.1.2",
2828
"@types/webpack-env": "^1.16",
29-
"svelte": "^3.0 || ^4.0"
29+
"svelte": "^3.0 || ^4.0 || ^5.0"
3030
}
3131
}

src/Svelte/assets/src/render_controller.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Controller } from '@hotwired/stimulus';
2-
import type { SvelteComponent } from 'svelte';
2+
import type { SvelteComponent, ComponentConstructorOptions, ComponentType } from 'svelte';
3+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
34

45
export default class extends Controller<Element & { root?: SvelteComponent }> {
56
private app: SvelteComponent;
@@ -17,7 +18,7 @@ export default class extends Controller<Element & { root?: SvelteComponent }> {
1718
intro: Boolean,
1819
};
1920

20-
connect() {
21+
async connect() {
2122
this.element.innerHTML = '';
2223

2324
this.props = this.propsValue ?? undefined;
@@ -29,8 +30,7 @@ export default class extends Controller<Element & { root?: SvelteComponent }> {
2930

3031
this._destroyIfExists();
3132

32-
// @see https://svelte.dev/docs#run-time-client-side-component-api-creating-a-component
33-
this.app = new Component({
33+
this.app = await this.mountSvelteComponent(Component, {
3434
target: this.element,
3535
props: this.props,
3636
intro: this.intro,
@@ -64,4 +64,18 @@ export default class extends Controller<Element & { root?: SvelteComponent }> {
6464
};
6565
this.dispatch(name, { detail, prefix: 'svelte' });
6666
}
67+
68+
// @see https://svelte.dev/docs#run-time-client-side-component-api-creating-a-component
69+
private async mountSvelteComponent(
70+
Component: ComponentType,
71+
options: ComponentConstructorOptions
72+
): Promise<SvelteComponent> {
73+
if (SVELTE_VERSION?.startsWith('5')) {
74+
// @ts-ignore
75+
const { mount } = await import('svelte');
76+
return mount(Component, options);
77+
}
78+
79+
return new Component(options);
80+
}
6781
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<script>
1+
<script>
22
import { fade } from 'svelte/transition';
33
export let name = 'without props';
4-
</script>
5-
6-
<div transition:fade|global={{ duration: 100 }}>
7-
<div>Hello {name}</div>
4+
</script>
5+
6+
<div transition:fade|global={{ duration: 100 }}>
7+
<div>Hello {name}</div>
88
</div>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { name = 'without props' } = $props();
3+
</script>
4+
5+
<div>
6+
<div>Hello {name}</div>
7+
</div>

src/Svelte/assets/test/register_controller.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99

1010
import { registerSvelteControllerComponents } from '../src/register_controller';
1111
import MyComponent from './fixtures/MyComponent.svelte';
12+
import MyComponentSvelte5 from './fixtures/MyComponentSvelte5.svelte';
1213
import RequireContext = __WebpackModuleApi.RequireContext;
1314

1415
const createFakeFixturesContext = (): RequireContext => {
1516
const files: any = {
1617
'./MyComponent.svelte': { default: MyComponent },
18+
'./MyComponentSvelte5.svelte': { default: MyComponentSvelte5 },
1719
};
1820

1921
const context = (id: string): any => files[id];
@@ -32,5 +34,7 @@ describe('registerSvelteControllerComponents', () => {
3234
expect(resolveComponent).not.toBeUndefined();
3335
expect(resolveComponent('MyComponent')).toBe(MyComponent);
3436
expect(resolveComponent('MyComponent')).not.toBeUndefined();
37+
expect(resolveComponent('MyComponentSvelte5')).toBe(MyComponentSvelte5);
38+
expect(resolveComponent('MyComponentSvelte5')).not.toBeUndefined();
3539
});
3640
});

src/Svelte/assets/test/render_controller.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getByTestId, waitFor } from '@testing-library/dom';
1212
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
1313
import SvelteController from '../src/render_controller';
1414
import MyComponent from './fixtures/MyComponent.svelte';
15+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
1516

1617
// Controller used to check the actual controller was properly booted
1718
class CheckController extends Controller {
@@ -83,7 +84,8 @@ describe('SvelteController', () => {
8384
await waitFor(() => expect(component.innerHTML).toContain('<div><div>Hello without props</div></div>'));
8485
});
8586

86-
it('connect with props and intro', async () => {
87+
// Disabled for Svelte 5 : https://github.com/sveltejs/svelte/issues/11280
88+
it.skipIf(SVELTE_VERSION >= '5')('connect with props and intro', async () => {
8789
const container = mountDOM(`
8890
<div data-testid="component" id="container"
8991
data-controller="check svelte"
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import { Application } from '@hotwired/stimulus';
11+
import { getByTestId, waitFor } from '@testing-library/dom';
12+
import { clearDOM, mountDOM } from '@symfony/stimulus-testing';
13+
import SvelteController from '../src/render_controller';
14+
import MyComponentSvelte5 from './fixtures/MyComponentSvelte5.svelte';
15+
import { VERSION as SVELTE_VERSION } from 'svelte/compiler';
16+
17+
const startStimulus = () => {
18+
const application = Application.start();
19+
application.register('svelte', SvelteController);
20+
21+
return application;
22+
};
23+
24+
(window as any).resolveSvelteComponent = () => {
25+
return MyComponentSvelte5;
26+
};
27+
28+
describe.skipIf(SVELTE_VERSION < '5')('Svelte5Controller', () => {
29+
let application: Application;
30+
31+
afterEach(() => {
32+
clearDOM();
33+
application.stop();
34+
});
35+
36+
it('connect with props', async () => {
37+
const container = mountDOM(`
38+
<div data-testid="component"
39+
data-controller="check svelte 5"
40+
data-svelte-component-value="Svelte5Component"
41+
data-svelte-props-value="{&quot;name&quot;: &quot;Svelte 5 !&quot;}" />
42+
`);
43+
44+
const component = getByTestId(container, 'component');
45+
46+
application = startStimulus();
47+
48+
await waitFor(() => expect(component.innerHTML).toContain('<div><div>Hello Svelte 5 !</div></div>'));
49+
});
50+
51+
it('connect without props', async () => {
52+
const container = mountDOM(`
53+
<div data-testid="component" id="container"
54+
data-controller="check svelte 5"
55+
data-svelte-component-value="Svelte5Component" />
56+
`);
57+
58+
const component = getByTestId(container, 'component');
59+
60+
application = startStimulus();
61+
62+
await waitFor(() => expect(component.innerHTML).toContain('<div><div>Hello without props</div></div>'));
63+
});
64+
});

src/Svelte/assets/vitest.config.mjs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import { defineConfig, mergeConfig } from 'vitest/config';
33
import { svelte } from '@sveltejs/vite-plugin-svelte';
44
import configShared from '../../../vitest.config.mjs'
55

6-
export default mergeConfig(
7-
configShared,
8-
defineConfig({
9-
plugins: [svelte()],
10-
})
11-
);
6+
export default defineConfig(configEnv => mergeConfig(
7+
configShared,
8+
defineConfig({
9+
plugins: [svelte()],
10+
resolve: {
11+
conditions: configEnv.mode === 'test' ? ['browser'] : [],
12+
},
13+
})
14+
))

src/Svelte/doc/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Svelte is a JavaScript framework for building user interfaces.
88
Symfony UX Svelte provides tools to render Svelte components from Twig,
99
handling rendering and data transfers.
1010

11-
Symfony UX Svelte supports Svelte 3 and Svelte 4.
11+
Symfony UX Svelte supports Svelte 3, 4 and 5.
1212

1313
Installation
1414
------------
@@ -51,7 +51,7 @@ Next, install a package to help Svelte:
5151
That's it! Any files inside ``assets/svelte/controllers/`` can now be rendered as
5252
Svelte components.
5353

54-
If you are using Svelte 4, you will have to add ``browser``, ``import`` and ``svelte``
54+
If you are using Svelte 4 or 5, you will have to add ``browser``, ``import`` and ``svelte``
5555
to the ``conditionNames`` array. This is necessary as per `the Svelte 4 migration guide`_
5656
for bundlers such as webpack, to ensure that lifecycle callbacks are internally invoked.
5757

0 commit comments

Comments
 (0)