From de04118ea937fa99f780990924865f70c8eae49f Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 4 Oct 2024 10:09:54 +0300 Subject: [PATCH 1/5] fix: HMR for translation files * Clears resource bundle cache on translation resource redeployment * Fires a HMR event to the browser when translations are reloaded Limitation: Only supports DefaultI18NHandler where the paths are known. If you have a custom I18N handler, you might need a custom HMR supporting HotswapListener. For #20118 Requires https://github.com/vaadin/hilla/pull/2795 to fully fix the issue --- .../com/vaadin/flow/hotswap/Hotswapper.java | 27 +++++++++++++++++-- .../flow/internal/BrowserLiveReload.java | 13 ++++++++- vaadin-dev-server/package.json | 2 +- .../src/main/frontend/vaadin-dev-tools.ts | 16 ++++++++++- .../base/devserver/DebugWindowConnection.java | 11 ++++++++ vaadin-dev-server/vite.config.js | 6 ++--- 6 files changed, 67 insertions(+), 8 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java index 73776c031f8..7c46f12f00a 100644 --- a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -51,6 +52,9 @@ import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinSession; +import elemental.json.Json; +import elemental.json.JsonObject; + /** * Entry point for application classes hot reloads. *

@@ -165,13 +169,32 @@ public void onHotswap(URI[] createdResources, URI[] modifiedResources, "Hotswap resources change event ignored because VaadinService has been destroyed."); return; } - // no-op for the moment, just logging for debugging purpose - // entry point for future implementations, like reloading I18n provider if (LOGGER.isTraceEnabled()) { LOGGER.trace( "Created resources: {}, modified resources: {}, deletedResources: {}.", createdResources, modifiedResources, deletedResources); } + + if (anyMatches(".*/vaadin-i18n/.*\\.properties", createdResources, + modifiedResources, deletedResources)) { + // Clear resource bundle cache so that translations (and other + // resources) are reloaded + ResourceBundle.clearCache(); + // Trigger any potential Hilla translation updates + liveReload.sendHmrEvent("translations-update", Json.createObject()); + } + + } + + private boolean anyMatches(String regexp, URI[]... resources) { + for (URI[] uris : resources) { + for (URI uri : uris) { + if (uri.toString().matches(regexp)) { + return true; + } + } + } + return false; } private void onHotswapInternal(HashSet> classes, diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java b/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java index e6baabbc8b9..fb07cb343f8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java @@ -17,9 +17,10 @@ import org.atmosphere.cpr.AtmosphereResource; -import com.vaadin.flow.component.UI; import com.vaadin.flow.server.communication.FragmentedMessageHolder; +import elemental.json.JsonObject; + /** * Provides a way to reload browser tabs via web socket connection passed as a * {@link AtmosphereResource}. @@ -117,4 +118,14 @@ default void refresh(boolean refreshLayouts) { */ void onMessage(AtmosphereResource resource, String msg); + /** + * Send a client side HMR event. + * + * @param event + * the event name + * @param eventData + * the event data + */ + void sendHmrEvent(String event, JsonObject eventData); + } diff --git a/vaadin-dev-server/package.json b/vaadin-dev-server/package.json index 5bb6fd40ac5..a61161dffa8 100644 --- a/vaadin-dev-server/package.json +++ b/vaadin-dev-server/package.json @@ -15,7 +15,7 @@ "@web/dev-server-esbuild": "^0.3.3", "prettier": "^2.8.4", "tslib": "^2.5.3", - "vite": "^4.1.4" + "vite": "^5.4.8" }, "dependencies": { "construct-style-sheets-polyfill": "^3.1.0", diff --git a/vaadin-dev-server/src/main/frontend/vaadin-dev-tools.ts b/vaadin-dev-server/src/main/frontend/vaadin-dev-tools.ts index c2fca024438..b3d3cd11ae6 100644 --- a/vaadin-dev-server/src/main/frontend/vaadin-dev-tools.ts +++ b/vaadin-dev-server/src/main/frontend/vaadin-dev-tools.ts @@ -77,6 +77,10 @@ type DevToolsConf = { liveReloadPort: number; token?: string; }; + +// @ts-ignore +const hmrClient: any = import.meta.hot ? import.meta.hot.hmrClient : undefined; + @customElement('vaadin-dev-tools') export class VaadinDevTools extends LitElement { unhandledMessages: ServerMessage[] = []; @@ -711,12 +715,22 @@ export class VaadinDevTools extends LitElement { } handleFrontendMessage(message: ServerMessage) { if (message.command === 'featureFlags') { - } else if (handleLicenseMessage(message)) { + } else if (handleLicenseMessage(message) || this.handleHmrMessage(message)) { } else { this.unhandledMessages.push(message); } } + handleHmrMessage(message: ServerMessage): boolean { + if (message.command !== 'hmr') { + return false; + } + if (hmrClient) { + hmrClient.notifyListeners(message.data.event, message.data.eventData); + } + return true; + } + getDedicatedWebSocketUrl(): string | undefined { function getAbsoluteUrl(relative: string) { // Use innerHTML to obtain an absolute URL diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java index 71c5dbafa91..e823ffb1d60 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/DebugWindowConnection.java @@ -381,4 +381,15 @@ public void clearFragmentedMessage(AtmosphereResource resource) { resources.put(ref, new FragmentedMessage()); } + @Override + public void sendHmrEvent(String event, JsonObject eventData) { + JsonObject msg = Json.createObject(); + msg.put("command", "hmr"); + JsonObject data = Json.createObject(); + msg.put("data", data); + data.put("event", event); + data.put("eventData", eventData); + broadcast(msg); + } + } diff --git a/vaadin-dev-server/vite.config.js b/vaadin-dev-server/vite.config.js index 006aae76ee1..a313312f6af 100644 --- a/vaadin-dev-server/vite.config.js +++ b/vaadin-dev-server/vite.config.js @@ -2,8 +2,6 @@ import { fileURLToPath } from 'url'; import { defineConfig } from 'vite'; import typescript from '@rollup/plugin-typescript'; -const { execSync } = require('child_process'); - export default defineConfig({ build: { // Write output to resources to include it in Maven package @@ -27,5 +25,7 @@ export default defineConfig({ /^@vaadin.*/, ] } - } + }, + // Preserve import.meta.hot in the built file so it can be replaced in the application instead + define: { 'import.meta.hot': 'import.meta.hot' } }); From 8f527cf82fcc20d125a791933f6e461cdae3fb30 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 4 Oct 2024 15:43:47 +0300 Subject: [PATCH 2/5] format --- .../main/java/com/vaadin/flow/internal/BrowserLiveReload.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java b/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java index fb07cb343f8..0870121324f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java +++ b/flow-server/src/main/java/com/vaadin/flow/internal/BrowserLiveReload.java @@ -120,7 +120,7 @@ default void refresh(boolean refreshLayouts) { /** * Send a client side HMR event. - * + * * @param event * the event name * @param eventData From 51a04cdd6cda2f56caca52a624391e12727d6f12 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Fri, 4 Oct 2024 15:51:03 +0200 Subject: [PATCH 3/5] trigger Flow UIs refresh --- .../src/main/java/com/vaadin/flow/hotswap/Hotswapper.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java index 7c46f12f00a..30976bb0905 100644 --- a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java @@ -53,7 +53,6 @@ import com.vaadin.flow.server.VaadinSession; import elemental.json.Json; -import elemental.json.JsonObject; /** * Entry point for application classes hot reloads. @@ -180,6 +179,11 @@ public void onHotswap(URI[] createdResources, URI[] modifiedResources, // Clear resource bundle cache so that translations (and other // resources) are reloaded ResourceBundle.clearCache(); + // Trigger UI refresh + EnumMap> refreshStrategy = new EnumMap<>( + UIRefreshStrategy.class); + refreshStrategy.put(UIRefreshStrategy.REFRESH, List.of()); + triggerClientUpdate(refreshStrategy, false); // Trigger any potential Hilla translation updates liveReload.sendHmrEvent("translations-update", Json.createObject()); } From 80ac8b6474b509a0858bf164c56ce35f770da967 Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 4 Oct 2024 16:14:58 +0300 Subject: [PATCH 4/5] flow support --- .../com/vaadin/flow/hotswap/Hotswapper.java | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java index 30976bb0905..a7bb0157829 100644 --- a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java @@ -29,6 +29,7 @@ import java.util.ResourceBundle; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -179,13 +180,17 @@ public void onHotswap(URI[] createdResources, URI[] modifiedResources, // Clear resource bundle cache so that translations (and other // resources) are reloaded ResourceBundle.clearCache(); - // Trigger UI refresh - EnumMap> refreshStrategy = new EnumMap<>( - UIRefreshStrategy.class); - refreshStrategy.put(UIRefreshStrategy.REFRESH, List.of()); - triggerClientUpdate(refreshStrategy, false); + // Trigger any potential Hilla translation updates liveReload.sendHmrEvent("translations-update", Json.createObject()); + + // Trigger any potential Flow translation updates + List uis = new ArrayList<>(); + EnumMap> refreshActions = new EnumMap<>( + UIRefreshStrategy.class); + forEachActiveUI(ui -> uis.add(ui)); + refreshActions.put(UIRefreshStrategy.REFRESH, uis); + triggerClientUpdate(refreshActions, false); } } @@ -285,23 +290,25 @@ private EnumMap> computeRefreshStrategies( Set vaadinSessions, Set> changedClasses) { EnumMap> uisToRefresh = new EnumMap<>( UIRefreshStrategy.class); - for (VaadinSession session : vaadinSessions) { + forEachActiveUI(ui -> uisToRefresh + .computeIfAbsent(computeRefreshStrategy(ui, changedClasses), + k -> new ArrayList<>()) + .add(ui)); + + uisToRefresh.remove(UIRefreshStrategy.SKIP); + return uisToRefresh; + } + + private void forEachActiveUI(Consumer consumer) { + for (VaadinSession session : Set.copyOf(sessions)) { session.getLockInstance().lock(); try { session.getUIs().stream().filter(ui -> !ui.isClosing()) - .forEach( - ui -> uisToRefresh - .computeIfAbsent( - computeRefreshStrategy(ui, - changedClasses), - k -> new ArrayList<>()) - .add(ui)); + .forEach(consumer); } finally { session.getLockInstance().unlock(); } } - uisToRefresh.remove(UIRefreshStrategy.SKIP); - return uisToRefresh; } private UIRefreshStrategy computeRefreshStrategy(UI ui, From 248c050f381f5fadb3bb8c031df7522470a3e718 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Fri, 4 Oct 2024 16:33:25 +0200 Subject: [PATCH 5/5] detect correct refresh stragety per UI --- .../main/java/com/vaadin/flow/hotswap/Hotswapper.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java index a7bb0157829..ef704c1996f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java +++ b/flow-server/src/main/java/com/vaadin/flow/hotswap/Hotswapper.java @@ -185,11 +185,16 @@ public void onHotswap(URI[] createdResources, URI[] modifiedResources, liveReload.sendHmrEvent("translations-update", Json.createObject()); // Trigger any potential Flow translation updates - List uis = new ArrayList<>(); EnumMap> refreshActions = new EnumMap<>( UIRefreshStrategy.class); - forEachActiveUI(ui -> uis.add(ui)); - refreshActions.put(UIRefreshStrategy.REFRESH, uis); + forEachActiveUI(ui -> { + UIRefreshStrategy strategy = ui.getPushConfiguration() + .getPushMode().isEnabled() + ? UIRefreshStrategy.PUSH_REFRESH_CHAIN + : UIRefreshStrategy.REFRESH; + refreshActions.computeIfAbsent(strategy, k -> new ArrayList<>()) + .add(ui); + }); triggerClientUpdate(refreshActions, false); }