14 Commits
v2.32.0 ... 2.x

Author SHA1 Message Date
github-actions[bot]
05af0259f2 Update versions to 2.34.0 2026-03-22 22:21:50 +00:00
Hugo Alliaume
d610a2e021 [Autocomplete][Chartjs][Cropperjs][Dropzone][LazyImage][LiveComponent][Map][Notify][React][StimulusBundle][Svelte][Swup][TogglePassword][Translator][Turbo][Typed][Vue] Update package.json to 2.33.0
The job that release npm packages failed https://github.com/symfony/ux/actions/runs/23216748609/job/67479440402

it should be fixed by https://github.com/symfony/ux/pull/3400
2026-03-21 23:29:11 +01:00
Hugo Alliaume
66b32eaad0 Update tsdown & use @tsdown/css
This update simplifies the tsdown configuration, we do not need our custom plugin to minify CSS anymore (replaced by `css.minify = true`), and same for our hooks that rename the built CSS (replaced by `css.fileName`) 😍
2026-03-20 09:00:50 +01:00
Hugo Alliaume
581fe67a2a Update Vitest to ^4.1.0 2026-03-15 08:57:29 +01:00
Hugo Alliaume
90640ce587 Migrate from tsup (deprecated) to tsdown
tsup is deprecated in favor of tsdown.

Follow https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, and many (local) tries were I was not really happy with the files generated by tsdown/rolldown, we are finally having something extra good!

We have the benefits from https://github.com/symfony/ux/pull/2935, https://github.com/symfony/ux/pull/2944, but without their drawbacks. The code correctly follow the `es2022` target and does not do anything weird anymore with static properties (needed by controllers).

I (Claude) added a plugin to remove the region and JSDoc comments (except if they contain `@deprecated``), since I think we want to keep the code non-minified.
2026-02-28 09:33:37 +01:00
Hugo Alliaume
0401477482 Remove tsx dependency and rely on Node.js 22.18.0 native TypeScript runner 2026-02-27 20:17:39 +01:00
Hugo Alliaume
7eca9dbf13 Drop Biome.js for oxfmt and oxlint 2026-02-03 23:14:09 +01:00
Hugo Alliaume
0c630746cd Run PHP-CS-Fixer (no_useless_else & static_lambda) 2026-02-03 22:36:24 +01:00
Hugo Alliaume
75f6193026 [Autocomplete][Chartjs][Cropperjs][Dropzone][LazyImage][React][StimulusBundle][Svelte][Swup][TogglePassword][Translator][Turbo][Typed][Vue] Use Extension from DependencyInjection instead of HttpKernel 2026-01-31 08:23:54 +01:00
Hugo Alliaume
7d017977d5 Fix npm releases due to repository issue 2026-01-16 23:36:00 +01:00
Hugo Alliaume
f380e2a352 Update versions to 2.32.0 2026-01-16 23:35:37 +01:00
Hugo Alliaume
40e5dee2da Update root JS dependencies 2026-01-11 00:13:28 +01:00
Hugo Alliaume
591df71416 Add changelog entry for #3285 2026-01-10 10:22:44 +01:00
Hugo Alliaume
e1d125ac83 [StimulusBundle][Performance] Change AssetMapper excluded_patterns from **/controllers.json to */controllers.json
Related to https://github.com/symfony/symfony/issues/61771
2026-01-10 09:53:48 +01:00
19 changed files with 146 additions and 177 deletions

View File

@@ -1,5 +1,4 @@
Please do not submit any Pull Requests here. They will be closed.
---
## Please do not submit any Pull Requests here. They will be closed.
Please submit your PR here instead:
https://github.com/symfony/ux

View File

@@ -1,20 +1,20 @@
name: Close Pull Request
on:
pull_request_target:
types: [opened]
pull_request_target:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/ux
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!
run:
runs-on: ubuntu-latest
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: |
Thanks for your Pull Request! We love contributions.
However, you should instead open your PR on the main repository:
https://github.com/symfony/ux
This repository is what we call a "subtree split": a read-only subset of that main repository.
We're looking forward to your PR there!

View File

@@ -1,3 +1,3 @@
branches: ["2.x"]
maintained_branches: ["2.x"]
doc_dir: "doc"
branches: ['2.x']
maintained_branches: ['2.x']
doc_dir: 'doc'

View File

@@ -1,50 +1,54 @@
# CHANGELOG
## 2.33
- Change AssetMapper `excluded_patterns` from `**/controllers.json` to `*/controllers.json`
## 2.30
- Ensure compatibility with PHP 8.5
- Ensure compatibility with PHP 8.5
## 2.29.0
- Add Symfony 8 support
- Add Symfony 8 support
## 2.20.1
- Normalize Stimulus controller name in event name
- Normalize Stimulus controller name in event name
## 2.14.2
- Fix bug with finding UX Packages with non-standard project structure
- Fix bug with finding UX Packages with non-standard project structure
## 2.14.1
- Fixed bug with Stimulus controllers in subdirectories on Windows
- Fixed bug with Stimulus controllers in subdirectories on Windows
## 2.14.0
- Added Typescript controllers support
- Added Typescript controllers support
## 2.13.2
- Revert "Change JavaScript package to `type: module`"
- Revert "Change JavaScript package to `type: module`"
## 2.13.0
- Normalize parameters names given to twig helper 'stimulus_action()'.
**BC Break**: previously, parameters given in camelCase (eg.
`bigCrocodile`) were incorrectly registered by the controller as
flatcase (`event.params.bigcrocodile`). This was fixed, which means
they are now correctly registered as camelCase
(`event.params.bigCrocodile`).
- Added AssetMapper 6.4 support.
- Add Symfony 7 support.
- Fix missing double dash in namespaced Stimulus outlets.
- Change JavaScript package to `type: module`
- Normalize parameters names given to twig helper 'stimulus_action()'.
**BC Break**: previously, parameters given in camelCase (eg.
`bigCrocodile`) were incorrectly registered by the controller as
flatcase (`event.params.bigcrocodile`). This was fixed, which means
they are now correctly registered as camelCase
(`event.params.bigCrocodile`).
- Added AssetMapper 6.4 support.
- Add Symfony 7 support.
- Fix missing double dash in namespaced Stimulus outlets.
- Change JavaScript package to `type: module`
## 2.10.0
- Handle Stimulus outlets
- Handle Stimulus outlets
## 2.9.0
- Introduce the bundle
- Introduce the bundle

View File

@@ -2,9 +2,9 @@
This bundle adds integration between Symfony, Stimulus and Symfony UX:
- A) Twig `stimulus_*` functions & filters to add Stimulus controllers, actions & targets in your templates;
- B) Integration with Symfony UX & AssetMapper;
- C) A helper service to build the Stimulus data attributes and use them in your services.
- A) Twig `stimulus_*` functions & filters to add Stimulus controllers, actions & targets in your templates;
- B) Integration with Symfony UX & AssetMapper;
- C) A helper service to build the Stimulus data attributes and use them in your services.
[Read the documentation][1]

View File

@@ -9,7 +9,7 @@ Read more at [symfony/ux#2708](https://github.com/symfony/ux/issues/2708).
## Resources
- [Documentation](https://symfony.com/bundles/StimulusBundle/current/index.html)
- [Report issues](https://github.com/symfony/ux/issues) and
[send Pull Requests](https://github.com/symfony/ux/pulls)
in the [main Symfony UX repository](https://github.com/symfony/ux)
- [Documentation](https://symfony.com/bundles/StimulusBundle/current/index.html)
- [Report issues](https://github.com/symfony/ux/issues) and
[send Pull Requests](https://github.com/symfony/ux/pulls)
in the [main Symfony UX repository](https://github.com/symfony/ux)

View File

@@ -1,15 +1,13 @@
import { ControllerConstructor } from '@hotwired/stimulus';
import { ControllerConstructor } from "@hotwired/stimulus";
interface EagerControllersCollection {
[key: string]: ControllerConstructor;
[key: string]: ControllerConstructor;
}
interface LazyControllersCollection {
[key: string]: () => Promise<{
default: ControllerConstructor;
}>;
[key: string]: () => Promise<{
default: ControllerConstructor;
}>;
}
declare const eagerControllers: EagerControllersCollection;
declare const lazyControllers: LazyControllersCollection;
declare const isApplicationDebug = false;
export { type EagerControllersCollection, type LazyControllersCollection, eagerControllers, isApplicationDebug, lazyControllers };
export { EagerControllersCollection, LazyControllersCollection, eagerControllers, isApplicationDebug, lazyControllers };

View File

@@ -1,8 +1,4 @@
const eagerControllers = {};
const lazyControllers = {};
const isApplicationDebug = false;
export {
eagerControllers,
isApplicationDebug,
lazyControllers
};
export { eagerControllers, isApplicationDebug, lazyControllers };

View File

@@ -1,7 +1,5 @@
import { Application } from '@hotwired/stimulus';
import { EagerControllersCollection, LazyControllersCollection } from './controllers.js';
import { Application } from "@hotwired/stimulus";
import { EagerControllersCollection, LazyControllersCollection } from "./controllers.js";
declare const loadControllers: (application: Application, eagerControllers: EagerControllersCollection, lazyControllers: LazyControllersCollection) => void;
declare const startStimulusApp: () => Application;
export { loadControllers, startStimulusApp };
export { loadControllers, startStimulusApp };

146
assets/dist/loader.js vendored
View File

@@ -1,98 +1,70 @@
import { Application } from "@hotwired/stimulus";
import {
eagerControllers,
isApplicationDebug,
lazyControllers
} from "./controllers.js";
import { eagerControllers, isApplicationDebug, lazyControllers } from "./controllers.js";
const controllerAttribute = "data-controller";
const loadControllers = (application, eagerControllers2, lazyControllers2) => {
for (const name in eagerControllers2) {
registerController(name, eagerControllers2[name], application);
}
const lazyControllerHandler = new StimulusLazyControllerHandler(
application,
lazyControllers2
);
lazyControllerHandler.start();
const loadControllers = (application, eagerControllers, lazyControllers) => {
for (const name in eagerControllers) registerController(name, eagerControllers[name], application);
new StimulusLazyControllerHandler(application, lazyControllers).start();
};
const startStimulusApp = () => {
const application = Application.start();
application.debug = isApplicationDebug;
loadControllers(application, eagerControllers, lazyControllers);
return application;
const application = Application.start();
application.debug = isApplicationDebug;
loadControllers(application, eagerControllers, lazyControllers);
return application;
};
var StimulusLazyControllerHandler = class {
constructor(application, lazyControllers) {
this.application = application;
this.lazyControllers = lazyControllers;
}
start() {
this.lazyLoadExistingControllers(document.documentElement);
this.lazyLoadNewControllers(document.documentElement);
}
lazyLoadExistingControllers(element) {
Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
loadLazyController(name) {
if (!this.lazyControllers[name]) return;
const controllerLoader = this.lazyControllers[name];
delete this.lazyControllers[name];
if (!canRegisterController(name, this.application)) return;
this.application.logDebugActivity(name, "lazy:loading");
controllerLoader().then((controllerModule) => {
this.application.logDebugActivity(name, "lazy:loaded");
registerController(name, controllerModule.default, this.application);
}).catch((error) => {
console.error(`Error loading controller "${name}":`, error);
});
}
lazyLoadNewControllers(element) {
if (Object.keys(this.lazyControllers).length === 0) return;
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) switch (type) {
case "attributes":
if (attributeName === controllerAttribute && target.getAttribute(controllerAttribute)) extractControllerNamesFrom(target).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
break;
case "childList": this.lazyLoadExistingControllers(target);
}
}).observe(element, {
attributeFilter: [controllerAttribute],
subtree: true,
childList: true
});
}
};
class StimulusLazyControllerHandler {
constructor(application, lazyControllers2) {
this.application = application;
this.lazyControllers = lazyControllers2;
}
start() {
this.lazyLoadExistingControllers(document.documentElement);
this.lazyLoadNewControllers(document.documentElement);
}
lazyLoadExistingControllers(element) {
Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).flatMap(extractControllerNamesFrom).forEach((controllerName) => this.loadLazyController(controllerName));
}
loadLazyController(name) {
if (!this.lazyControllers[name]) {
return;
}
const controllerLoader = this.lazyControllers[name];
delete this.lazyControllers[name];
if (!canRegisterController(name, this.application)) {
return;
}
this.application.logDebugActivity(name, "lazy:loading");
controllerLoader().then((controllerModule) => {
this.application.logDebugActivity(name, "lazy:loaded");
registerController(name, controllerModule.default, this.application);
}).catch((error) => {
console.error(`Error loading controller "${name}":`, error);
});
}
lazyLoadNewControllers(element) {
if (Object.keys(this.lazyControllers).length === 0) {
return;
}
new MutationObserver((mutationsList) => {
for (const { attributeName, target, type } of mutationsList) {
switch (type) {
case "attributes": {
if (attributeName === controllerAttribute && target.getAttribute(controllerAttribute)) {
extractControllerNamesFrom(target).forEach(
(controllerName) => this.loadLazyController(controllerName)
);
}
break;
}
case "childList": {
this.lazyLoadExistingControllers(target);
}
}
}
}).observe(element, {
attributeFilter: [controllerAttribute],
subtree: true,
childList: true
});
}
}
function registerController(name, controller, application) {
if (canRegisterController(name, application)) {
application.register(name, controller);
}
if (canRegisterController(name, application)) application.register(name, controller);
}
function extractControllerNamesFrom(element) {
const controllerNameValue = element.getAttribute(controllerAttribute);
if (!controllerNameValue) {
return [];
}
return controllerNameValue.split(/\s+/).filter((content) => content.length);
const controllerNameValue = element.getAttribute(controllerAttribute);
if (!controllerNameValue) return [];
return controllerNameValue.split(/\s+/).filter((content) => content.length);
}
function canRegisterController(name, application) {
return !application.router.modulesByIdentifier.has(name);
return !application.router.modulesByIdentifier.has(name);
}
export {
loadControllers,
startStimulusApp
};
export { loadControllers, startStimulusApp };

View File

@@ -3,24 +3,22 @@
"description": "Integration of @hotwired/stimulus into Symfony",
"private": true,
"license": "MIT",
"version": "2.31.0",
"version": "2.34.0",
"keywords": [
"symfony-ux"
],
"homepage": "https://ux.symfony.com/stimulus",
"repository": "https://github.com/symfony/stimulus-bundle",
"repository": "https://github.com/symfony/ux",
"type": "module",
"files": [
"dist"
],
"main": "dist/loader.js",
"scripts": {
"build": "tsx ../../../bin/build_package.ts .",
"watch": "tsx ../../../bin/build_package.ts . --watch",
"build": "node ../../../bin/build_package.ts .",
"watch": "node ../../../bin/build_package.ts . --watch",
"test": "pnpm run test:unit && pnpm run test:browser",
"test:unit": "../../../bin/unit_test_package.sh .",
"check": "biome check",
"ci": "biome ci"
"test:unit": "../../../bin/unit_test_package.sh ."
},
"symfony": {
"needsPackageAsADependency": false,
@@ -39,8 +37,7 @@
"@testing-library/user-event": "^14.6.1",
"jsdom": "^26.1.0",
"tslib": "^2.8.1",
"tsx": "^4.20.3",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
"vitest": "^4.1.0"
}
}

View File

@@ -66,10 +66,12 @@ class StimulusLazyControllerHandler {
private lazyLoadExistingControllers(element: Element) {
Array.from(element.querySelectorAll(`[${controllerAttribute}]`))
.flatMap(extractControllerNamesFrom)
.forEach((controllerName) => this.loadLazyController(controllerName));
.forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
private loadLazyController(name: string) {
private loadLazyController(name: string): void {
if (!this.lazyControllers[name]) {
return;
}
@@ -106,9 +108,9 @@ class StimulusLazyControllerHandler {
attributeName === controllerAttribute &&
(target as Element).getAttribute(controllerAttribute)
) {
extractControllerNamesFrom(target as Element).forEach((controllerName) =>
this.loadLazyController(controllerName)
);
extractControllerNamesFrom(target as Element).forEach((controllerName) => {
this.loadLazyController(controllerName);
});
}
break;
@@ -144,6 +146,6 @@ function extractControllerNamesFrom(element: Element): string[] {
}
function canRegisterController(name: string, application: Application) {
// @ts-ignore
// @ts-expect-error
return !application.router.modulesByIdentifier.has(name);
}

3
assets/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../../../tsconfig.package.json"
}

View File

@@ -95,7 +95,7 @@ class StimulusLoaderJavaScriptCompiler implements AssetCompilerInterface
} 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)));
$lazyControllers[] = \sprintf('%s: () => Promise.all([import(%s), %s]).then((ret) => ret[0])', json_encode($name), $relativeImportPath, implode(', ', array_map(static fn ($path) => "import($path)", $autoImportPaths)));
}
continue;

View File

@@ -18,9 +18,9 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
/**
* @author Ryan Weaver <ryan@symfonycasts.com>
@@ -56,7 +56,7 @@ final class StimulusExtension extends Extension implements PrependExtensionInter
],
'excluded_patterns' => [
'*.d.ts',
'**/controllers.json',
'*/controllers.json',
],
],
]);

View File

@@ -141,7 +141,7 @@ class StimulusAttributes implements \Stringable, \IteratorAggregate
public function toArray(): array
{
$actions = array_map(function (array $actionData): string {
$actions = array_map(static function (array $actionData): string {
$controllerName = $actionData['controllerName'];
$actionName = $actionData['actionName'];
$eventName = $actionData['eventName'];

View File

@@ -27,7 +27,7 @@ class ControllersMapGeneratorTest extends TestCase
$mapper = $this->createMock(AssetMapperInterface::class);
$mapper->expects($this->any())
->method('getAssetFromSourcePath')
->willReturnCallback(function ($path) {
->willReturnCallback(static function ($path) {
if (str_ends_with($path, 'package-controller-first.js')) {
$logicalPath = 'fake-vendor/ux-package1/package-controller-first.js';
} elseif (str_ends_with($path, 'package-controller-second.js')) {
@@ -57,7 +57,7 @@ class ControllersMapGeneratorTest extends TestCase
if (class_exists(ImportMapConfigReader::class)) {
$autoImportLocator->expects($this->any())
->method('locateAutoImport')
->willReturnCallback(function ($path) {
->willReturnCallback(static function ($path) {
return new MappedControllerAutoImport('/path/to'.$path, false);
});
} else {

View File

@@ -42,10 +42,10 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
if (class_exists(ImportMapConfigReader::class)) {
// filter out items ending in .css
$importMapJsKeys = array_filter($importMapKeys, function ($key) {
$importMapJsKeys = array_filter($importMapKeys, static function ($key) {
return '.css' !== substr($key, -4);
});
$importMapCssKeys = array_filter($importMapKeys, function ($key) {
$importMapCssKeys = array_filter($importMapKeys, static function ($key) {
return '.css' === substr($key, -4);
});
sort($importMapJsKeys);
@@ -92,7 +92,7 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
], array_values($importMapCssKeys));
// "app" is the entry. So, all non-lazy controllers should be preloaded:
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) {
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(static function ($link) {
return $link->attr('href');
});
$this->assertCount(12, $preLoadHrefs);
@@ -135,7 +135,7 @@ class StimulusControllerLoaderFunctionalTest extends WebTestCase
], $importMapKeys);
// "app" & loader.js are pre-loaded. So, all non-lazy controllers should be preloaded:
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(function ($link) {
$preLoadHrefs = $crawler->filter('link[rel="modulepreload"]')->each(static function ($link) {
return $link->attr('href');
});
$this->assertCount(10, $preLoadHrefs);

View File

@@ -39,7 +39,7 @@ class UxControllersTwigRuntimeTest extends TestCase
$assetMapper = $this->createMock(AssetMapperInterface::class);
$assetMapper->expects($this->any())
->method('getAsset')
->willReturnCallback(function ($path) {
->willReturnCallback(static function ($path) {
if (str_starts_with($path, 'in/asset/mapper')) {
return new MappedAsset(basename($path), publicPath: '/assets/mapper/'.basename($path));
}