Repository: symfony/ux (php)

Issue:
[Leaflet] Error during installation 
Module build failed: Module not found:
"./vendor/symfony/ux-leaflet-map/assets/dist/map_controller.js" contains a reference to the file "@symfony/ux-map/abstract-map-controller".
This file can not be found, please check it for typos or update it if the file got moved.

"./vendor/symfony/ux-leaflet-map/assets/dist/map_controller.js" contains a reference to the file "leaflet/dist/leaflet.min.css".
This file can not be found, please check it for typos or update it if the file got moved.

Pull request:
[Map] Fix Google/Leaflet bridges when using Webpack Encore
| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no <!-- please update src/**/CHANGELOG.md files -->
| Issues        | Fix #2105 <!-- 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).
-->

This PR fixes #2105, and ensures Leaflet and GoogleMaps bridges are working fine with Webpack Encore.

To be sure:
1. I've created a new Symfony app (no `--webapp`), 
2. Installed Encore, Stimulus, and Map
3. Added `.addAliases({ 'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css' })` and `.enableStimulusBridge('./assets/controllers.json')` in `webpack.config.js`
4. Created the Map (`new Map()`) and rendered it (`ux_map(map, { style: 'height: ...' })`)

I am able to display a Leaflet map:
<img width="1568" alt="image" src="https://github.com/user-attachments/assets/0a44eb8e-8346-452a-9598-2774af17f0ad">

And a GoogleMap maps: 
<img width="1537" alt="image" src="https://github.com/user-attachments/assets/2fae715e-fa2d-41c3-a566-886a340f9c1d">

---

The Map rendered on ux.symfony.com is also working (after updating the `importmap.php` as documented):

<img width="1543" alt="image" src="https://github.com/user-attachments/assets/c869cad6-e48b-4c24-8ea3-490e7109a05d">


Patch:
diff --git a/src/Map/CHANGELOG.md b/src/Map/CHANGELOG.md
index d4894408..de37e53e 100644
--- a/src/Map/CHANGELOG.md
+++ b/src/Map/CHANGELOG.md
@@ -7,6 +7,8 @@
 -   Add `ux_map` Twig function (replaces `render_map` with a more flexible 
     interface)
 -   Add `<twig:ux:map />` Twig component
+-   The importmap entry `@symfony/ux-map/abstract-map-controller` can be removed
+    from your importmap, it is no longer needed. 
 
 ## 2.19
 
diff --git a/src/Map/assets/package.json b/src/Map/assets/package.json
index 2561ef55..4cd6dd52 100644
--- a/src/Map/assets/package.json
+++ b/src/Map/assets/package.json
@@ -8,8 +8,7 @@
     "types": "dist/abstract_map_controller.d.ts",
     "symfony": {
         "importmap": {
-            "@hotwired/stimulus": "^3.0.0",
-            "@symfony/ux-map/abstract-map-controller": "path:%PACKAGE%/dist/abstract_map_controller.js"
+            "@hotwired/stimulus": "^3.0.0"
         }
     },
     "peerDependencies": {
diff --git a/src/Map/src/Bridge/Google/CHANGELOG.md b/src/Map/src/Bridge/Google/CHANGELOG.md
index 1d4c12f8..46f7d49c 100644
--- a/src/Map/src/Bridge/Google/CHANGELOG.md
+++ b/src/Map/src/Bridge/Google/CHANGELOG.md
@@ -1,5 +1,12 @@
 # CHANGELOG
 
+## 2.20
+
+### BC Breaks
+
+-   Renamed importmap entry `@symfony/ux-google-map/map-controller` to `@symfony/ux-google-map`, 
+    you will need to update your importmap.
+
 ## 2.19
 
 -   Bridge added
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
index aef508dc..8a2d2abc 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.d.ts
@@ -1,5 +1,5 @@
-import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
-import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
+import AbstractMapController from '@symfony/ux-map';
+import type { Point, MarkerDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 type MapOptions = Pick<google.maps.MapOptions, 'mapId' | 'gestureHandling' | 'backgroundColor' | 'disableDoubleClickZoom' | 'zoomControl' | 'zoomControlOptions' | 'mapTypeControl' | 'mapTypeControlOptions' | 'streetViewControl' | 'streetViewControlOptions' | 'fullscreenControl' | 'fullscreenControlOptions'>;
 export default class extends AbstractMapController<MapOptions, google.maps.Map, google.maps.marker.AdvancedMarkerElement, google.maps.InfoWindow> {
diff --git a/src/Map/src/Bridge/Google/assets/dist/map_controller.js b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
index 5a741632..d35e6512 100644
--- a/src/Map/src/Bridge/Google/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Google/assets/dist/map_controller.js
@@ -1,8 +1,48 @@
-import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
+import { Controller } from '@hotwired/stimulus';
 import { Loader } from '@googlemaps/js-api-loader';
 
+let default_1$1 = class default_1 extends Controller {
+    constructor() {
+        super(...arguments);
+        this.markers = [];
+        this.infoWindows = [];
+    }
+    connect() {
+        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
+        this.dispatchEvent('pre-connect', { options });
+        this.map = this.doCreateMap({ center, zoom, options });
+        markers.forEach((marker) => this.createMarker(marker));
+        if (fitBoundsToMarkers) {
+            this.doFitBoundsToMarkers();
+        }
+        this.dispatchEvent('connect', {
+            map: this.map,
+            markers: this.markers,
+            infoWindows: this.infoWindows,
+        });
+    }
+    createMarker(definition) {
+        this.dispatchEvent('marker:before-create', { definition });
+        const marker = this.doCreateMarker(definition);
+        this.dispatchEvent('marker:after-create', { marker });
+        this.markers.push(marker);
+        return marker;
+    }
+    createInfoWindow({ definition, marker, }) {
+        this.dispatchEvent('info-window:before-create', { definition, marker });
+        const infoWindow = this.doCreateInfoWindow({ definition, marker });
+        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
+        this.infoWindows.push(infoWindow);
+        return infoWindow;
+    }
+};
+default_1$1.values = {
+    providerOptions: Object,
+    view: Object,
+};
+
 let _google;
-class default_1 extends AbstractMapController {
+class default_1 extends default_1$1 {
     async connect() {
         if (!_google) {
             _google = { maps: {} };
diff --git a/src/Map/src/Bridge/Google/assets/package.json b/src/Map/src/Bridge/Google/assets/package.json
index e5f5ea74..8b8dfdbc 100644
--- a/src/Map/src/Bridge/Google/assets/package.json
+++ b/src/Map/src/Bridge/Google/assets/package.json
@@ -18,7 +18,7 @@
         "importmap": {
             "@hotwired/stimulus": "^3.0.0",
             "@googlemaps/js-api-loader": "^1.16.6",
-            "@symfony/ux-google-map/map-controller": "path:%PACKAGE%/dist/map_controller.js"
+            "@symfony/ux-google-map": "path:%PACKAGE%/dist/map_controller.js"
         }
     },
     "peerDependencies": {
diff --git a/src/Map/src/Bridge/Google/assets/src/map_controller.ts b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
index 1e1a1a06..7eed733f 100644
--- a/src/Map/src/Bridge/Google/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Google/assets/src/map_controller.ts
@@ -7,8 +7,8 @@
  * file that was distributed with this source code.
  */
 
-import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
-import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
+import AbstractMapController from '@symfony/ux-map';
+import type { Point, MarkerDefinition } from '@symfony/ux-map';
 import type { LoaderOptions } from '@googlemaps/js-api-loader';
 import { Loader } from '@googlemaps/js-api-loader';
 
diff --git a/src/Map/src/Bridge/Leaflet/CHANGELOG.md b/src/Map/src/Bridge/Leaflet/CHANGELOG.md
index 1d4c12f8..e380bd8e 100644
--- a/src/Map/src/Bridge/Leaflet/CHANGELOG.md
+++ b/src/Map/src/Bridge/Leaflet/CHANGELOG.md
@@ -1,5 +1,12 @@
 # CHANGELOG
 
+## 2.20
+
+### BC Breaks
+
+-   Renamed importmap entry `@symfony/ux-leaflet-map/map-controller` to `@symfony/ux-leaflet-map`, 
+    you will need to update your importmap. 
+
 ## 2.19
 
 -   Bridge added
diff --git a/src/Map/src/Bridge/Leaflet/README.md b/src/Map/src/Bridge/Leaflet/README.md
index 9618f9ba..a267776d 100644
--- a/src/Map/src/Bridge/Leaflet/README.md
+++ b/src/Map/src/Bridge/Leaflet/README.md
@@ -90,6 +90,34 @@ export default class extends Controller
 }
 ```
 
+## Known issues
+
+### Unable to find `leaflet/dist/leaflet.min.css` file when using Webpack Encore
+
+When using Webpack Encore with the Leaflet bridge, you may encounter the following error:
+```
+Module build failed: Module not found:
+"./node_modules/.pnpm/file+vendor+symfony+ux-leaflet-map+assets_@hotwired+stimulus@3.0.0_leaflet@1.9.4/node_modules/@symfony/ux-leaflet-map/dist/map_controller.js" contains a reference to the file "leaflet/dist/leaflet.min.css".
+This file can not be found, please check it for typos or update it if the file got moved.
+
+Entrypoint app = runtime.67292354.js 488.0777101a.js app.b75294ae.css app.0975a86d.js
+webpack compiled with 1 error
+ ELIFECYCLE  Command failed with exit code 1.
+```
+
+That's because the Leaflet's Stimulus controller references the `leaflet/dist/leaflet.min.css` file, 
+which exists on [jsDelivr](https://www.jsdelivr.com/package/npm/leaflet) (used by the Symfony AssetMapper component),
+but does not in the [`leaflet` npm package](https://www.npmjs.com/package/leaflet).
+The correct path is `leaflet/dist/leaflet.css`, but it is not possible to fix it because it would break compatibility 
+with the Symfony AssetMapper component.
+
+As a workaround, you can configure Webpack Encore to add an alias for the `leaflet/dist/leaflet.min.css` file:
+```js
+Encore.addAliases({
+  'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
+})
+```
+
 ## Resources
 
 - [Documentation](https://symfony.com/bundles/ux-map/current/index.html)
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
index c152fb4c..04e533b4 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.d.ts
@@ -1,5 +1,5 @@
-import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
-import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
+import AbstractMapController from '@symfony/ux-map';
+import type { Point, MarkerDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
 import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
diff --git a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
index 466d40c5..36331506 100644
--- a/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
+++ b/src/Map/src/Bridge/Leaflet/assets/dist/map_controller.js
@@ -1,8 +1,48 @@
-import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
+import { Controller } from '@hotwired/stimulus';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
 
-class map_controller extends AbstractMapController {
+class default_1 extends Controller {
+    constructor() {
+        super(...arguments);
+        this.markers = [];
+        this.infoWindows = [];
+    }
+    connect() {
+        const { center, zoom, options, markers, fitBoundsToMarkers } = this.viewValue;
+        this.dispatchEvent('pre-connect', { options });
+        this.map = this.doCreateMap({ center, zoom, options });
+        markers.forEach((marker) => this.createMarker(marker));
+        if (fitBoundsToMarkers) {
+            this.doFitBoundsToMarkers();
+        }
+        this.dispatchEvent('connect', {
+            map: this.map,
+            markers: this.markers,
+            infoWindows: this.infoWindows,
+        });
+    }
+    createMarker(definition) {
+        this.dispatchEvent('marker:before-create', { definition });
+        const marker = this.doCreateMarker(definition);
+        this.dispatchEvent('marker:after-create', { marker });
+        this.markers.push(marker);
+        return marker;
+    }
+    createInfoWindow({ definition, marker, }) {
+        this.dispatchEvent('info-window:before-create', { definition, marker });
+        const infoWindow = this.doCreateInfoWindow({ definition, marker });
+        this.dispatchEvent('info-window:after-create', { infoWindow, marker });
+        this.infoWindows.push(infoWindow);
+        return infoWindow;
+    }
+}
+default_1.values = {
+    providerOptions: Object,
+    view: Object,
+};
+
+class map_controller extends default_1 {
     connect() {
         L.Marker.prototype.options.icon = L.divIcon({
             html: '<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" stroke-linecap="round" clip-rule="evenodd" viewBox="0 0 500 820"><defs><linearGradient id="__sf_ux_map_gradient_marker_fill" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(0 -37.57 37.57 0 416.45 541)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#126FC6"/><stop offset="1" stop-color="#4C9CD1"/></linearGradient><linearGradient id="__sf_ux_map_gradient_marker_border" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(0 -19.05 19.05 0 414.48 522.49)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2E6C97"/><stop offset="1" stop-color="#3883B7"/></linearGradient></defs><circle cx="252.31" cy="266.24" r="83.99" fill="#fff"/><path fill="url(#__sf_ux_map_gradient_marker_fill)" stroke="url(#__sf_ux_map_gradient_marker_border)" stroke-width="1.1" d="M416.54 503.61c-6.57 0-12.04 5.7-12.04 11.87 0 2.78 1.56 6.3 2.7 8.74l9.3 17.88 9.26-17.88c1.13-2.43 2.74-5.79 2.74-8.74 0-6.18-5.38-11.87-11.96-11.87Zm0 7.16a4.69 4.69 0 1 1-.02 9.4 4.69 4.69 0 0 1 .02-9.4Z" transform="translate(-7889.1 -9807.44) scale(19.54)"/></svg>',
diff --git a/src/Map/src/Bridge/Leaflet/assets/package.json b/src/Map/src/Bridge/Leaflet/assets/package.json
index dc95b75a..3dd5663a 100644
--- a/src/Map/src/Bridge/Leaflet/assets/package.json
+++ b/src/Map/src/Bridge/Leaflet/assets/package.json
@@ -18,7 +18,7 @@
         "importmap": {
             "@hotwired/stimulus": "^3.0.0",
             "leaflet": "^1.9.4",
-            "@symfony/ux-leaflet-map/map-controller": "path:%PACKAGE%/dist/map_controller.js"
+            "@symfony/ux-leaflet-map": "path:%PACKAGE%/dist/map_controller.js"
         }
     },
     "peerDependencies": {
diff --git a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
index ec6a78dd..3e342b77 100644
--- a/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
+++ b/src/Map/src/Bridge/Leaflet/assets/src/map_controller.ts
@@ -1,5 +1,5 @@
-import AbstractMapController from '@symfony/ux-map/abstract-map-controller';
-import type { Point, MarkerDefinition } from '@symfony/ux-map/abstract-map-controller';
+import AbstractMapController from '@symfony/ux-map';
+import type { Point, MarkerDefinition } from '@symfony/ux-map';
 import 'leaflet/dist/leaflet.min.css';
 import * as L from 'leaflet';
 import type { MapOptions as LeafletMapOptions, MarkerOptions, PopupOptions } from 'leaflet';
diff --git a/ux.symfony.com/importmap.php b/ux.symfony.com/importmap.php
index af8af902..90273c85 100644
--- a/ux.symfony.com/importmap.php
+++ b/ux.symfony.com/importmap.php
@@ -190,9 +190,6 @@ return [
     'chart.js' => [
         'version' => '4.4.3',
     ],
-    '@symfony/ux-map/abstract-map-controller' => [
-        'path' => './vendor/symfony/ux-map/assets/dist/abstract_map_controller.js',
-    ],
     'leaflet' => [
         'version' => '1.9.4',
     ],
@@ -200,7 +197,7 @@ return [
         'version' => '1.9.4',
         'type' => 'css',
     ],
-    '@symfony/ux-leaflet-map/map-controller' => [
+    '@symfony/ux-leaflet-map' => [
         'path' => './vendor/symfony/ux-leaflet-map/assets/dist/map_controller.js',
     ],
 ];


Test patch:
diff --git a/src/Map/src/Bridge/Google/assets/vitest.config.mjs b/src/Map/src/Bridge/Google/assets/vitest.config.mjs
index dde4635d..c957798d 100644
--- a/src/Map/src/Bridge/Google/assets/vitest.config.mjs
+++ b/src/Map/src/Bridge/Google/assets/vitest.config.mjs
@@ -6,7 +6,7 @@ export default mergeConfig(
     defineConfig({
         resolve: {
             alias: {
-                '@symfony/ux-map/abstract-map-controller': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
+                '@symfony/ux-map': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
             },
         },
         define: {
diff --git a/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs b/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs
index 7072015e..8fb88b88 100644
--- a/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs
+++ b/src/Map/src/Bridge/Leaflet/assets/vitest.config.mjs
@@ -6,7 +6,7 @@ export default mergeConfig(
     defineConfig({
         resolve: {
             alias: {
-                '@symfony/ux-map/abstract-map-controller': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
+                '@symfony/ux-map': __dirname + '/../../../../assets/src/abstract_map_controller.ts',
                 'leaflet/dist/leaflet.min.css': 'leaflet/dist/leaflet.css',
             },
         },