Repository: symfony/ux
Pull request #2326 (2024-11-02 19:12:47)

[DX] Add per-package Yarn scripts (build, watch, test, lint, ...)
| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | Fix #... <!-- prefix each issue number with "Fix #", no need to create an issue if none exist, explain below instead -->
| License       | MIT

<!--
Replace this notice by a description of your feature/bugfix.
This will help reviewers and should be a good start for the documentation.

Additionally (see https://symfony.com/releases):
 - Always add tests and ensure they pass.
 - For new features, provide some code snippets to help understand usage.
 - Features and deprecations must be submitted against branch main.
 - Changelog entry should follow https://symfony.com/doc/current/contributing/code/conventions.html#writing-a-changelog-entry
 - Never break backward compatibility (see https://symfony.com/bc).
-->

When working in UX Map, it was very painful to build or watch modifications on `.ts` files, I had investigate how the building process worked, and had to run Rollup like this, which is far from ideal (not documented and not obvious):  
```
./node_modules/.bin/rollup -c --environment INPUT_FILE:src/Map/assets/src/abstract_map_controller.ts -w
./node_modules/.bin/rollup -c --environment INPUT_FILE:src/Map/src/Bridge/Google/assets/src/map_controller.ts -w
./node_modules/.bin/rollup -c --environment INPUT_FILE:src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts -w
```

And I also felt sorry for @rrr63 when he worked on adding Polygons on Map (#2162)...

This PR improves the way developers will work on UX, and makes their lives easier.

**Before**:
- `yarn build` compiled the assets from ALL packages, it was not possible to build packages from only one package (which is useful if you work on a single package)
- no `yarn watch`
- `yarn test` runned tests from ALL packages, like `yarn build`, it was not possible to run tests for only one package

**Now:** 
- at the project root, `build`/`test`/`lint`/`format`/`check-lint`/`check-format` scripts will run on all assets **from all packages**. And it will be faster than before, when processing was sequential, but now it's parallelized.
- at a package root (ex:  `src/Map/assets`), `build`/`test`/`lint`/`format`/`check-lint`/`check-format` scripts will run on all assets **from this package only**
- `build` and `watch` scripts handles both TypeScript and CSS files in a single command

This is a first step to what we spoke about with @smnandre to write a contribution guide.
It is now much more easier and friendlier to tell a developer to run `yarn watch` inside a package root, instead of telling it to run `./node_modules/.bin/rollup -c --environment INPUT_FILE:src/Map/src/Bridge/Google/assets/src/map_controller.ts -w` (and more, if you have multiple files).

<img width="1210" alt="image" src="https://github.com/user-attachments/assets/f40d4efc-439d-4f83-a0f8-611b1a64b334">



Patch:
diff --git a/bin/build_javascript.js b/bin/build_javascript.js
deleted file mode 100644
index 7f176223..00000000
--- a/bin/build_javascript.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * This file is used to compile the TypeScript files in the assets/src directory
- * of each package.
- *
- * It allows each package to spawn its own rollup process, which is necessary
- * to keep memory usage down.
- */
-const { spawnSync } = require('child_process');
-const glob = require('glob');
-
-const files = [
-    // custom handling for React
-    'src/React/assets/src/loader.ts',
-    'src/React/assets/src/components.ts',
-    // custom handling for Svelte
-    'src/Svelte/assets/src/loader.ts',
-    'src/Svelte/assets/src/components.ts',
-    // custom handling for Vue
-    'src/Vue/assets/src/loader.ts',
-    'src/Vue/assets/src/components.ts',
-    // custom handling for StimulusBundle
-    'src/StimulusBundle/assets/src/loader.ts',
-    'src/StimulusBundle/assets/src/controllers.ts',
-    // custom handling for Bridge
-    ...glob.sync('src/*/src/Bridge/*/assets/src/*controller.ts'),
-    ...glob.sync('src/*/assets/src/*controller.ts'),
-];
-
-files.forEach((file) => {
-    const result = spawnSync('node', [
-        'node_modules/.bin/rollup',
-        '-c',
-        '--environment',
-        `INPUT_FILE:${file}`,
-    ], {
-        stdio: 'inherit',
-        shell: true
-    });
-
-    if (result.error) {
-        console.error(`Error compiling ${file}:`, result.error);
-    }
-});
diff --git a/bin/build_package.js b/bin/build_package.js
new file mode 100644
index 00000000..ee3054d2
--- /dev/null
+++ b/bin/build_package.js
@@ -0,0 +1,128 @@
+/**
+ * This file is used to compile the assets from an UX package.
+ */
+
+const { parseArgs } = require('node:util');
+const path = require('node:path');
+const fs = require('node:fs');
+const glob = require('glob');
+const rollup = require('rollup');
+const CleanCSS = require('clean-css');
+const { getRollupConfiguration } = require('./rollup');
+
+const args = parseArgs({
+    allowPositionals: true,
+    options: {
+        watch: {
+            type: 'boolean',
+            description: 'Watch the source files for changes and rebuild when necessary.',
+        },
+    },
+});
+
+async function main() {
+    const packageRoot = path.resolve(process.cwd(), args.positionals[0]);
+
+    if (!fs.existsSync(packageRoot)) {
+        console.error(`The package directory "${packageRoot}" does not exist.`);
+        process.exit(1);
+    }
+
+    if (!fs.existsSync(path.join(packageRoot, 'package.json'))) {
+        console.error(`The package directory "${packageRoot}" does not contain a package.json file.`);
+        process.exit(1);
+    }
+
+    const packageData = require(path.join(packageRoot, 'package.json'));
+    const packageName = packageData.name;
+    const srcDir = path.join(packageRoot, 'src');
+    const distDir = path.join(packageRoot, 'dist');
+
+    if (!fs.existsSync(srcDir)) {
+        console.error(`The package directory "${packageRoot}" does not contain a "src" directory.`);
+        process.exit(1);
+    }
+
+    if (fs.existsSync(distDir)) {
+        console.log(`Cleaning up the "${distDir}" directory...`);
+        await fs.promises.rm(distDir, { recursive: true });
+        await fs.promises.mkdir(distDir);
+    }
+
+    const inputScriptFiles = [
+        ...glob.sync(path.join(srcDir, '*controller.ts')),
+        ...(['@symfony/ux-react', '@symfony/ux-vue', '@symfony/ux-svelte'].includes(packageName)
+            ? [path.join(srcDir, 'loader.ts'), path.join(srcDir, 'components.ts')]
+            : []),
+        ...(packageName === '@symfony/stimulus-bundle'
+            ? [path.join(srcDir, 'loader.ts'), path.join(srcDir, 'controllers.ts')]
+            : []),
+    ];
+
+    const inputStyleFile = packageData.config?.css_source;
+    const buildCss = async () => {
+        const inputStyleFileDist = inputStyleFile
+            ? path.resolve(distDir, `${path.basename(inputStyleFile, '.css')}.min.css`)
+            : undefined;
+        if (!inputStyleFile) {
+            return;
+        }
+
+        console.log('Minifying CSS...');
+        const css = await fs.promises.readFile(inputStyleFile, 'utf-8');
+        const minified = new CleanCSS().minify(css).styles;
+        await fs.promises.writeFile(inputStyleFileDist, minified);
+    };
+
+    if (inputScriptFiles.length === 0) {
+        console.error(
+            `No input files found for package "${packageName}" (directory "${packageRoot}").\nEnsure you have at least a file matching the pattern "src/*_controller.ts", or manually specify input files in "${__filename}" file.`
+        );
+        process.exit(1);
+    }
+
+    const rollupConfig = getRollupConfiguration({ packageRoot, inputFiles: inputScriptFiles });
+
+    if (args.values.watch) {
+        console.log(
+            `Watching for JavaScript${inputStyleFile ? ' and CSS' : ''} files modifications in "${srcDir}" directory...`
+        );
+
+        if (inputStyleFile) {
+            rollupConfig.plugins = (rollupConfig.plugins || []).concat({
+                name: 'watcher',
+                buildStart() {
+                    this.addWatchFile(inputStyleFile);
+                },
+            });
+        }
+
+        const watcher = rollup.watch(rollupConfig);
+        watcher.on('event', ({ result }) => {
+            if (result) {
+                result.close();
+            }
+        });
+        watcher.on('change', async (id, { event }) => {
+            if (event === 'update') {
+                console.log('Files were modified, rebuilding...');
+            }
+
+            if (inputStyleFile && id === inputStyleFile) {
+                await buildCss();
+            }
+        });
+    } else {
+        console.log(`Building JavaScript files from ${packageName} package...`);
+        const start = Date.now();
+
+        const bundle = await rollup.rollup(rollupConfig);
+        await bundle.write(rollupConfig.output);
+
+        await buildCss();
+
+        console.log(`Done in ${((Date.now() - start) / 1000).toFixed(3)} seconds.`);
+    }
+}
+
+main();
diff --git a/bin/build_styles.js b/bin/build_styles.js
deleted file mode 100644
index ad45bb34..00000000
--- a/bin/build_styles.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Script to "build" the source CSS files to their final destination.
- */
-
-const glob = require('glob');
-const { exec } = require('child_process');
-const path = require('path');
-const fs = require('fs');
-
-let pattern = 'src/*/assets/package.json';
-
-// Use glob to find all matching package.json files
-glob(pattern, function (err, files) {
-    if (err) {
-        console.error('Error while finding package.json files:', err);
-        process.exit(1);
-    }
-
-    // Loop over all files
-    files.forEach(file => {
-        // Read the package.json file
-        const pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
-
-        // Get the css source
-        const cssSourceRelative = pkg.config && pkg.config.css_source;
-
-        if (!cssSourceRelative) {
-            return;
-        }
-        // Construct the output path
-        const cssSource = path.join(path.dirname(file), cssSourceRelative);
-        const outputDir = path.join(path.dirname(file), 'dist');
-        const outputFilename = path.basename(cssSource, '.css') + '.min.css';
-        const output = path.join(outputDir, outputFilename);
-
-        // Run the clean-css-cli command
-        exec(`yarn run cleancss -o ${output} ${cssSource}`, function (err) {
-            if (err) {
-                console.error(`Error while minifying ${cssSource}:`, err);
-                return;
-            }
-
-            console.log(`Minified ${cssSource} to ${output}`);
-        });
-    });
-});
diff --git a/bin/rollup.js b/bin/rollup.js
new file mode 100644
index 00000000..a75c3036
--- /dev/null
+++ b/bin/rollup.js
@@ -0,0 +1,134 @@
+const fs = require('node:fs');
+const path = require('node:path');
+const resolve = require('@rollup/plugin-node-resolve');
+const commonjs = require('@rollup/plugin-commonjs');
+const typescript = require('@rollup/plugin-typescript');
+const glob = require('glob');
+
+/**
+ * Guarantees that any files imported from a peer dependency are treated as an external.
+ *
+ * For example, if we import `chart.js/auto`, that would not normally
+ * match the "chart.js" we pass to the "externals" config. This plugin
+ * catches that case and adds it as an external.
+ *
+ * Inspired by https://github.com/oat-sa/rollup-plugin-wildcard-external
+ */
+const wildcardExternalsPlugin = (peerDependencies) => ({
+    name: 'wildcard-externals',
+    resolveId(source, importer) {
+        if (importer) {
+            let matchesExternal = false;
+            peerDependencies.forEach((peerDependency) => {
+                if (source.includes(`/${peerDependency}/`)) {
+                    matchesExternal = true;
+                }
+            });
+
+            if (matchesExternal) {
+                return {
+                    id: source,
+                    external: true,
+                    moduleSideEffects: true,
+                };
+            }
+        }
+
+        return null; // other ids should be handled as usually
+    },
+});
+
+/**
+ * Moves the generated TypeScript declaration files to the correct location.
+ *
+ * This could probably be configured in the TypeScript plugin.
+ */
+const moveTypescriptDeclarationsPlugin = (packageRoot) => ({
+    name: 'move-ts-declarations',
+    writeBundle: async () => {
+        const isBridge = packageRoot.includes('src/Bridge');
+        const globPattern = path.join('dist', '**', 'assets', 'src', '**/*.d.ts');
+        const files = glob.sync(globPattern);
+
+        files.forEach((file) => {
+            const relativePath = file;
+            // a bit odd, but remove first 7 or 4 directories, which will leave only the relative path to the file
+            // ex: dist/Chartjs/assets/src/controller.d.ts' => 'dist/controller.d.ts'
+            const targetFile = relativePath.replace(
+                `${relativePath
+                    .split('/')
+                    .slice(1, isBridge ? 7 : 4)
+                    .join('/')}/`,
+                ''
+            );
+            if (!fs.existsSync(path.dirname(targetFile))) {
+                fs.mkdirSync(path.dirname(targetFile), { recursive: true });
+            }
+            fs.renameSync(file, targetFile);
+        });
+    },
+});
+
+/**
+ * @param {String} packageRoot
+ * @param {Array<String>} inputFiles
+ */
+function getRollupConfiguration({ packageRoot, inputFiles }) {
+    const packagePath = path.join(packageRoot, 'package.json');
+    const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
+    const peerDependencies = [
+        '@hotwired/stimulus',
+        ...(packageData.peerDependencies ? Object.keys(packageData.peerDependencies) : []),
+    ];
+
+    inputFiles.forEach((file) => {
+        // custom handling for StimulusBundle
+        if (file.includes('StimulusBundle/assets/src/loader.ts')) {
+            peerDependencies.push('./controllers.js');
+        }
+
+        // React, Vue
+        if (file.includes('assets/src/loader.ts')) {
+            peerDependencies.push('./components.js');
+        }
+    });
+
+    const outDir = path.join(packageRoot, 'dist');
+
+    return {
+        input: inputFiles,
+        output: {
+            dir: outDir,
+            entryFileNames: '[name].js',
+            format: 'esm',
+        },
+        external: peerDependencies,
+        plugins: [
+            resolve(),
+            typescript({
+                filterRoot: '.',
+                tsconfig: path.join(__dirname, '..', 'tsconfig.json'),
+                include: [
+                    'src/**/*.ts',
+                    // TODO: Remove for the next major release
+                    // "@rollup/plugin-typescript" v11.0.0 fixed an issue (https://github.com/rollup/plugins/pull/1310) that
+                    // cause a breaking change for UX React users, the dist file requires "react-dom/client" instead of "react-dom"
+                    // and it will break for users using the Symfony AssetMapper without Symfony Flex (for automatic "importmap.php" upgrade).
+                    '**/node_modules/react-dom/client.js',
+                ],
+                compilerOptions: {
+                    outDir: outDir,
+                    declaration: true,
+                    emitDeclarationOnly: true,
+                },
+            }),
+            commonjs(),
+            wildcardExternalsPlugin(peerDependencies),
+            moveTypescriptDeclarationsPlugin(packageRoot),
+        ],
+    };
+}
+
+module.exports = {
+    getRollupConfiguration,
+};
diff --git a/biome.json b/biome.json
index a4f7f0f3..36d42169 100644
--- a/biome.json
+++ b/biome.json
@@ -4,6 +4,8 @@
         "include": [
             "*.json",
             "*.md",
+            "bin/*.js",
+            "test/*.js",
             "src/*/*.json",
             "src/*/*/md",
             "src/*/assets/src/**",
@@ -13,7 +15,7 @@
             "src/*/src/Bridge/*/assets/src/**",
             "src/*/src/Bridge/*/assets/test/**"
         ],
-        "ignore": ["**/composer.json", "**/vendor", "**/package.json", "**/node_modules"]
+        "ignore": ["**/composer.json", "**/vendor", "**/package.json", "**/node_modules", "**/var"]
     },
     "linter": {
         "rules": {
diff --git a/package.json b/package.json
index 3811ae9c..0952bdd9 100644
--- a/package.json
+++ b/package.json
@@ -6,8 +6,8 @@
         "src/*/src/Bridge/*/assets"
     ],
     "scripts": {
-        "build": "node bin/build_javascript.js && node bin/build_styles.js",
-        "test": "bin/run-vitest-all.sh",
+        "build": "yarn workspaces foreach -Apt run build",
+        "test": "yarn workspaces foreach -Apt run test",
         "lint": "biome lint --write",
         "format": "biome format --write",
         "check-lint": "biome lint",
@@ -24,7 +24,7 @@
         "@rollup/plugin-typescript": "^11.1.6",
         "@symfony/stimulus-testing": "^2.0.1",
         "@vitest/browser": "^2.1.1",
-        "clean-css-cli": "^5.6.2",
+        "clean-css": "^5.3.3",
         "playwright": "^1.47.0",
         "rollup": "^4.22.5",
         "tslib": "^2.6.3",
diff --git a/rollup.config.js b/rollup.config.js
deleted file mode 100644
index 7ab3b404..00000000
--- a/rollup.config.js
+++ /dev/null
@@ -1,115 +0,0 @@
-const resolve = require('@rollup/plugin-node-resolve');
-const commonjs = require('@rollup/plugin-commonjs');
-const typescript = require('@rollup/plugin-typescript');
-const fs = require('fs');
-const glob = require('glob');
-const path = require('path');
-
-/**
- * Guarantees that any files imported from a peer dependency are treated as an external.
- *
- * For example, if we import `chart.js/auto`, that would not normally
- * match the "chart.js" we pass to the "externals" config. This plugin
- * catches that case and adds it as an external.
- *
- * Inspired by https://github.com/oat-sa/rollup-plugin-wildcard-external
- */
-const wildcardExternalsPlugin = (peerDependencies) => ({
-    name: 'wildcard-externals',
-    resolveId(source, importer) {
-        if (importer) {
-            let matchesExternal = false;
-            peerDependencies.forEach((peerDependency) => {
-                if (source.includes(`/${peerDependency}/`)) {
-                    matchesExternal = true;
-                }
-            });
-
-            if (matchesExternal) {
-                return {
-                    id: source,
-                    external: true,
-                    moduleSideEffects: true
-                };
-            }
-        }
-
-        return null; // other ids should be handled as usually
-    }
-});
-
-/**
- * Moves the generated TypeScript declaration files to the correct location.
- *
- * This could probably be configured in the TypeScript plugin.
- */
-const moveTypescriptDeclarationsPlugin = (packagePath) => ({
-    name: 'move-ts-declarations',
-    writeBundle: async () => {
-        const isBridge = packagePath.includes('src/Bridge');
-        const globPattern = isBridge
-            ? path.join(packagePath, 'dist', packagePath.replace(/^src\//, ''), '**/*.d.ts')
-            : path.join(packagePath, 'dist', '*', 'assets', 'src', '**/*.d.ts')
-        const files = glob.sync(globPattern);
-        files.forEach((file) => {
-            // a bit odd, but remove first 7 or 13 directories, which will leave
-            // only the relative path to the file
-            const relativePath = file.split('/').slice(isBridge ? 13 : 7).join('/');
-
-            const targetFile = path.join(packagePath, 'dist', relativePath);
-            if (!fs.existsSync(path.dirname(targetFile))) {
-                fs.mkdirSync(path.dirname(targetFile), { recursive: true });
-            }
-            fs.renameSync(file, targetFile);
-        });
-    }
-});
-
-const file = process.env.INPUT_FILE;
-const packageRoot = path.join(file, '..', '..');
-const packagePath = path.join(packageRoot, 'package.json');
-const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
-const peerDependencies = [
-    '@hotwired/stimulus',
-    ...(packageData.peerDependencies ? Object.keys(packageData.peerDependencies) : [])
-];
-
-// custom handling for StimulusBundle
-if (file.includes('StimulusBundle/assets/src/loader.ts')) {
-    peerDependencies.push('./controllers.js');
-}
-// React, Vue
-if (file.includes('assets/src/loader.ts')) {
-    peerDependencies.push('./components.js');
-}
-
-module.exports = {
-    input: file,
-    output: {
-        file: path.join(packageRoot, 'dist', path.basename(file, '.ts') + '.js'),
-        format: 'esm',
-    },
-    external: peerDependencies,
-    plugins: [
-        resolve(),
-        typescript({
-            filterRoot: packageRoot,
-            include: [
-                'src/**/*.ts', 
-                // TODO: Remove for the next major release
-                // "@rollup/plugin-typescript" v11.0.0 fixed an issue (https://github.com/rollup/plugins/pull/1310) that 
-                // cause a breaking change for UX React users, the dist file requires "react-dom/client" instead of "react-dom"
-                // and it will break for users using the Symfony AssetMapper without Symfony Flex (for automatic "importmap.php" upgrade).
-                '**/node_modules/react-dom/client.js'
-            ],
-            compilerOptions: {
-                outDir: '.',
-                declaration: true,
-                emitDeclarationOnly: true,
-            }
-        }),
-        commonjs(),
-        wildcardExternalsPlugin(peerDependencies),
-        moveTypescriptDeclarationsPlugin(packageRoot),
-    ],
-};
diff --git a/src/Autocomplete/assets/package.json b/src/Autocomplete/assets/package.json
index 8af1c434..a7a238c5 100644
--- a/src/Autocomplete/assets/package.json
+++ b/src/Autocomplete/assets/package.json
@@ -5,6 +5,15 @@
     "types": "dist/controller.d.ts",
     "version": "1.0.0",
     "license": "MIT",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "autocomplete": {
diff --git a/src/Chartjs/assets/package.json b/src/Chartjs/assets/package.json
index ed730622..bfbab3f8 100644
--- a/src/Chartjs/assets/package.json
+++ b/src/Chartjs/assets/package.json
@@ -6,6 +6,15 @@
     "type": "module",
     "main": "dist/controller.js",
     "types": "dist/controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "chart": {
diff --git a/src/Cropperjs/assets/package.json b/src/Cropperjs/assets/package.json
index 2133ce56..ee4b4e29 100644
--- a/src/Cropperjs/assets/package.json
+++ b/src/Cropperjs/assets/package.json
@@ -8,6 +8,15 @@
     "config": {
         "css_source": "src/style.css"
     },
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "cropper": {
diff --git a/src/Dropzone/assets/package.json b/src/Dropzone/assets/package.json
index 1fc19df7..03c23dca 100644
--- a/src/Dropzone/assets/package.json
+++ b/src/Dropzone/assets/package.json
@@ -8,6 +8,15 @@
     "config": {
         "css_source": "src/style.css"
     },
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "dropzone": {
diff --git a/src/LazyImage/assets/package.json b/src/LazyImage/assets/package.json
index cc320117..7f2c8d85 100644
--- a/src/LazyImage/assets/package.json
+++ b/src/LazyImage/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.1.0",
     "main": "dist/controller.js",
     "types": "dist/controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "lazy-image": {
diff --git a/src/LiveComponent/assets/dist/Component/plugins/QueryStringPluging.d.ts b/src/LiveComponent/assets/dist/Component/plugins/QueryStringPluging.d.ts
deleted file mode 100644
index 6f9feeed..00000000
--- a/src/LiveComponent/assets/dist/Component/plugins/QueryStringPluging.d.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import Component from '../index';
-import { PluginInterface } from './PluginInterface';
-export default class implements PluginInterface {
-    private element;
-    private mapping;
-    attachToComponent(component: Component): void;
-    private registerBindings;
-    private updateUrl;
-}
diff --git a/src/LiveComponent/assets/package.json b/src/LiveComponent/assets/package.json
index a0d5dba4..8e5ece78 100644
--- a/src/LiveComponent/assets/package.json
+++ b/src/LiveComponent/assets/package.json
@@ -8,6 +8,15 @@
         "css_source": "styles/live.css"
     },
     "license": "MIT",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "live": {
diff --git a/src/Map/assets/package.json b/src/Map/assets/package.json
index 4cd6dd52..69ebc71b 100644
--- a/src/Map/assets/package.json
+++ b/src/Map/assets/package.json
@@ -6,6 +6,15 @@
     "type": "module",
     "main": "dist/abstract_map_controller.js",
     "types": "dist/abstract_map_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "importmap": {
             "@hotwired/stimulus": "^3.0.0"
diff --git a/src/Map/src/Bridge/Google/assets/package.json b/src/Map/src/Bridge/Google/assets/package.json
index 8b8dfdbc..17322cf9 100644
--- a/src/Map/src/Bridge/Google/assets/package.json
+++ b/src/Map/src/Bridge/Google/assets/package.json
@@ -6,6 +6,15 @@
     "type": "module",
     "main": "dist/map_controller.js",
     "types": "dist/map_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../../../../bin/build_package.js .",
+        "watch": "node ../../../../../../bin/build_package.js . --watch",
+        "test": "../../../../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "map": {
diff --git a/src/Map/src/Bridge/Leaflet/assets/package.json b/src/Map/src/Bridge/Leaflet/assets/package.json
index 3dd5663a..1ac16b1d 100644
--- a/src/Map/src/Bridge/Leaflet/assets/package.json
+++ b/src/Map/src/Bridge/Leaflet/assets/package.json
@@ -6,6 +6,15 @@
     "type": "module",
     "main": "dist/map_controller.js",
     "types": "dist/map_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../../../../bin/build_package.js .",
+        "watch": "node ../../../../../../bin/build_package.js . --watch",
+        "test": "../../../../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "map": {
diff --git a/src/Notify/assets/package.json b/src/Notify/assets/package.json
index 5f47e196..37c98d79 100644
--- a/src/Notify/assets/package.json
+++ b/src/Notify/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.0.0",
     "main": "dist/controller.js",
     "types": "dist/controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "notify": {
diff --git a/src/React/assets/package.json b/src/React/assets/package.json
index a4f7f675..8f1b3d50 100644
--- a/src/React/assets/package.json
+++ b/src/React/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.0.0",
     "main": "dist/register_controller.js",
     "types": "dist/register_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "react": {
diff --git a/src/StimulusBundle/assets/package.json b/src/StimulusBundle/assets/package.json
index 03cf5905..4d0d2b40 100644
--- a/src/StimulusBundle/assets/package.json
+++ b/src/StimulusBundle/assets/package.json
@@ -4,6 +4,15 @@
     "version": "1.0.0",
     "license": "MIT",
     "main": "dist/loader.js",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "needsPackageAsADependency": false,
         "importmap": {
diff --git a/src/Svelte/assets/package.json b/src/Svelte/assets/package.json
index 95eabf51..7b80877f 100644
--- a/src/Svelte/assets/package.json
+++ b/src/Svelte/assets/package.json
@@ -4,6 +4,15 @@
     "main": "dist/register_controller.js",
     "version": "1.0.0",
     "license": "MIT",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "svelte": {
diff --git a/src/Swup/assets/package.json b/src/Swup/assets/package.json
index 5e45461f..1cabd23f 100644
--- a/src/Swup/assets/package.json
+++ b/src/Swup/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.1.0",
     "main": "dist/controller.js",
     "types": "dist/controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "swup": {
diff --git a/src/TogglePassword/assets/package.json b/src/TogglePassword/assets/package.json
index 928dd1ad..2e27fec2 100644
--- a/src/TogglePassword/assets/package.json
+++ b/src/TogglePassword/assets/package.json
@@ -8,6 +8,15 @@
     "config": {
         "css_source": "src/style.css"
     },
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "toggle-password": {
diff --git a/src/Translator/assets/package.json b/src/Translator/assets/package.json
index 53c12bbc..6fd84918 100644
--- a/src/Translator/assets/package.json
+++ b/src/Translator/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.0.0",
     "main": "dist/translator_controller.js",
     "types": "dist/translator_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "importmap": {
             "intl-messageformat": "^10.5.11",
diff --git a/src/Turbo/assets/package.json b/src/Turbo/assets/package.json
index 395ac24c..bf086784 100644
--- a/src/Turbo/assets/package.json
+++ b/src/Turbo/assets/package.json
@@ -6,6 +6,15 @@
     "version": "0.1.0",
     "main": "dist/turbo_controller.js",
     "types": "dist/turbo_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "turbo-core": {
diff --git a/src/Typed/assets/package.json b/src/Typed/assets/package.json
index e187dcab..d146fb63 100644
--- a/src/Typed/assets/package.json
+++ b/src/Typed/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.0.0",
     "main": "dist/controller.js",
     "types": "dist/controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "typed": {
diff --git a/src/Vue/assets/package.json b/src/Vue/assets/package.json
index 05ec27f6..9b20a09f 100644
--- a/src/Vue/assets/package.json
+++ b/src/Vue/assets/package.json
@@ -5,6 +5,15 @@
     "version": "1.0.0",
     "main": "dist/register_controller.js",
     "types": "dist/register_controller.d.ts",
+    "scripts": {
+        "build": "node ../../../bin/build_package.js .",
+        "watch": "node ../../../bin/build_package.js . --watch",
+        "test": "../../../bin/test_package.sh .",
+        "lint": "biome lint --write",
+        "format": "biome format --write",
+        "check-lint": "biome lint",
+        "check-format": "biome format"
+    },
     "symfony": {
         "controllers": {
             "vue": {
diff --git a/yarn.lock b/yarn.lock
index a40a02fc..27e8d369 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3879,7 +3879,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2":
+"anymatch@npm:^3.0.3":
   version: 3.1.3
   resolution: "anymatch@npm:3.1.3"
   dependencies:
@@ -4135,13 +4135,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"binary-extensions@npm:^2.0.0":
-  version: 2.2.0
-  resolution: "binary-extensions@npm:2.2.0"
-  checksum: 10c0/d73d8b897238a2d3ffa5f59c0241870043aa7471335e89ea5e1ff48edb7c2d0bb471517a3e4c5c3f4c043615caa2717b5f80a5e61e07503d51dc85cb848e665d
-  languageName: node
-  linkType: hard
-
 "brace-expansion@npm:^1.1.7":
   version: 1.1.11
   resolution: "brace-expansion@npm:1.1.11"
@@ -4179,7 +4172,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"braces@npm:^3.0.2, braces@npm:~3.0.2":
+"braces@npm:^3.0.2":
   version: 3.0.2
   resolution: "braces@npm:3.0.2"
   dependencies:
@@ -4390,25 +4383,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"chokidar@npm:^3.5.2":
-  version: 3.5.3
-  resolution: "chokidar@npm:3.5.3"
-  dependencies:
-    anymatch: "npm:~3.1.2"
-    braces: "npm:~3.0.2"
-    fsevents: "npm:~2.3.2"
-    glob-parent: "npm:~5.1.2"
-    is-binary-path: "npm:~2.1.0"
-    is-glob: "npm:~4.0.1"
-    normalize-path: "npm:~3.0.0"
-    readdirp: "npm:~3.6.0"
-  dependenciesMeta:
-    fsevents:
-      optional: true
-  checksum: 10c0/1076953093e0707c882a92c66c0f56ba6187831aa51bb4de878c1fec59ae611a3bf02898f190efec8e77a086b8df61c2b2a3ea324642a0558bdf8ee6c5dc9ca1
-  languageName: node
-  linkType: hard
-
 "chownr@npm:^2.0.0":
   version: 2.0.0
   resolution: "chownr@npm:2.0.0"
@@ -4449,26 +4423,12 @@ __metadata:
   languageName: node
   linkType: hard
 
-"clean-css-cli@npm:^5.6.2":
-  version: 5.6.2
-  resolution: "clean-css-cli@npm:5.6.2"
-  dependencies:
-    chokidar: "npm:^3.5.2"
-    clean-css: "npm:^5.3.2"
-    commander: "npm:7.x"
-    glob: "npm:^7.1.6"
-  bin:
-    cleancss: bin/cleancss
-  checksum: 10c0/125d6aa8d34bce0598bcca2a14ca0c5f33dfcb237de28770c7982888d978070efd5bb5007d4d7a89c7cca385b5ed7d2717fa64e6f4a91a47cacf85b7522babd7
-  languageName: node
-  linkType: hard
-
-"clean-css@npm:^5.3.2":
-  version: 5.3.2
-  resolution: "clean-css@npm:5.3.2"
+"clean-css@npm:^5.3.3":
+  version: 5.3.3
+  resolution: "clean-css@npm:5.3.3"
   dependencies:
     source-map: "npm:~0.6.0"
-  checksum: 10c0/315e0e81306524bd2c1905fa6823bf7658be40799b78f446e5e6922808718d2b80266fb3e96842a06176fa683bc2c1a0d2827b08d154e2f9cf136d7bda909d33
+  checksum: 10c0/381de7523e23f3762eb180e327dcc0cedafaf8cb1cd8c26b7cc1fc56e0829a92e734729c4f955394d65ed72fb62f82d8baf78af34b33b8a7d41ebad2accdd6fb
   languageName: node
   linkType: hard
 
@@ -4593,13 +4553,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"commander@npm:7.x":
-  version: 7.2.0
-  resolution: "commander@npm:7.2.0"
-  checksum: 10c0/8d690ff13b0356df7e0ebbe6c59b4712f754f4b724d4f473d3cc5b3fdcf978e3a5dc3078717858a2ceb50b0f84d0660a7f22a96cdc50fb877d0c9bb31593d23a
-  languageName: node
-  linkType: hard
-
 "commondir@npm:^1.0.1":
   version: 1.0.1
   resolution: "commondir@npm:1.0.1"
@@ -5637,15 +5590,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"glob-parent@npm:~5.1.2":
-  version: 5.1.2
-  resolution: "glob-parent@npm:5.1.2"
-  dependencies:
-    is-glob: "npm:^4.0.1"
-  checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee
-  languageName: node
-  linkType: hard
-
 "glob@npm:^10.2.2, glob@npm:^10.3.10":
   version: 10.4.5
   resolution: "glob@npm:10.4.5"
@@ -5662,7 +5606,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6":
+"glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4":
   version: 7.2.3
   resolution: "glob@npm:7.2.3"
   dependencies:
@@ -6048,15 +5992,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-binary-path@npm:~2.1.0":
-  version: 2.1.0
-  resolution: "is-binary-path@npm:2.1.0"
-  dependencies:
-    binary-extensions: "npm:^2.0.0"
-  checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38
-  languageName: node
-  linkType: hard
-
 "is-boolean-object@npm:^1.1.0":
   version: 1.1.2
   resolution: "is-boolean-object@npm:1.1.2"
@@ -6175,13 +6110,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-extglob@npm:^2.1.1":
-  version: 2.1.1
-  resolution: "is-extglob@npm:2.1.1"
-  checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912
-  languageName: node
-  linkType: hard
-
 "is-fullwidth-code-point@npm:^3.0.0":
   version: 3.0.0
   resolution: "is-fullwidth-code-point@npm:3.0.0"
@@ -6196,15 +6124,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"is-glob@npm:^4.0.1, is-glob@npm:~4.0.1":
-  version: 4.0.3
-  resolution: "is-glob@npm:4.0.3"
-  dependencies:
-    is-extglob: "npm:^2.1.1"
-  checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a
-  languageName: node
-  linkType: hard
-
 "is-lambda@npm:^1.0.1":
   version: 1.0.1
   resolution: "is-lambda@npm:1.0.1"
@@ -7843,7 +7762,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0":
+"normalize-path@npm:^3.0.0":
   version: 3.0.0
   resolution: "normalize-path@npm:3.0.0"
   checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046
@@ -8160,7 +8079,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1":
+"picomatch@npm:^2.0.4, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1":
   version: 2.3.1
   resolution: "picomatch@npm:2.3.1"
   checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be
@@ -8406,15 +8325,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"readdirp@npm:~3.6.0":
-  version: 3.6.0
-  resolution: "readdirp@npm:3.6.0"
-  dependencies:
-    picomatch: "npm:^2.2.1"
-  checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b
-  languageName: node
-  linkType: hard
-
 "redent@npm:^3.0.0":
   version: 3.0.0
   resolution: "redent@npm:3.0.0"
@@ -8773,7 +8683,7 @@ __metadata:
     "@rollup/plugin-typescript": "npm:^11.1.6"
     "@symfony/stimulus-testing": "npm:^2.0.1"
     "@vitest/browser": "npm:^2.1.1"
-    clean-css-cli: "npm:^5.6.2"
+    clean-css: "npm:^5.3.3"
     playwright: "npm:^1.47.0"
     rollup: "npm:^4.22.5"
     tslib: "npm:^2.6.3"


Test patch:
diff --git a/bin/run-vitest-all.sh b/bin/test_package.sh
similarity index 66%
rename from bin/run-vitest-all.sh
rename to bin/test_package.sh
index 74db629b..669e293c 100755
--- a/bin/run-vitest-all.sh
+++ b/bin/test_package.sh
@@ -1,8 +1,20 @@
 #!/bin/bash
 
+# This script is used to test an UX package.
+# It also handle the case where a package has multiple versions of a peerDependency defined.
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+PROJECT_DIR=$(dirname "$SCRIPT_DIR")
+
 # Flag to track if any test fails
 all_tests_passed=true
 
+# Check if we have at least one argument
+if [ $# -eq 0 ]
+  then
+    echo "No arguments supplied, please provide the package's path."
+fi
+
 # Check if jq is installed
 if ! command -v jq &> /dev/null; then
     echo "jq is required but not installed. Aborting."
@@ -11,16 +23,31 @@ fi
 
 runTestSuite() {
     echo -e "Running tests for $workspace...\n"
-    yarn workspace $workspace run -T vitest --run || { all_tests_passed=false; }
+    yarn run -T vitest --run || { all_tests_passed=false; }
 }
 
 processWorkspace() {
-    local workspace="$1"
-    local location="$2"
+    local location="$1"
 
-    echo -e "Processing workspace $workspace at location $location...\n"
+    if [ ! -d "$location" ]; then
+        echo "No directory found at $location"
+        return
+    fi
 
     package_json_path="$location/package.json"
+    if [ ! -f "$package_json_path" ]; then
+        echo "No package.json found at $package_json_path"
+        return
+    fi
+    
+    workspace=$(jq -r '.name' "$package_json_path")
+    if [ -z "$workspace" ]; then
+        echo "No name found in package.json at $package_json_path"
+        return
+    fi
+    
+    echo -e "Processing workspace $workspace at location $location...\n"
+
     echo "Checking '$package_json_path' for peerDependencies with multiple versions defined"
     deps_with_multiple_versions=$(jq -r '.peerDependencies | to_entries[] | select(.value | contains("||")) | .key' "$package_json_path")
 
@@ -48,21 +75,7 @@ processWorkspace() {
     fi
 }
 
-# Iterate over each workspace using process substitution
-while IFS= read -r workspace_info; do
-    # Split the workspace_info into workspace and location
-    workspace=$(echo "$workspace_info" | jq -r '.name')
-    location=$(echo "$workspace_info" | jq -r '.location')
-    
-    # Skip the root workspace
-    if [ $workspace == "null" ]; then
-        continue
-    fi
-
-    # Call the function to process the workspace
-    processWorkspace "$workspace" "$location"
-
-done < <(yarn workspaces list --json)
+processWorkspace "$(realpath "$PWD/$1")"
 
 # Check the flag at the end and exit with code 1 if any test failed
 if [ "$all_tests_passed" = false ]; then
diff --git a/test/setup.js b/test/setup.js
index ddd4655c..f60ad9b0 100644
--- a/test/setup.js
+++ b/test/setup.js
@@ -7,6 +7,4 @@
  * file that was distributed with this source code.
  */
 
-'use strict';
-
 import '@symfony/stimulus-testing/setup';