From de04118ea937fa99f780990924865f70c8eae49f Mon Sep 17 00:00:00 2001 From: Artur Signell Date: Fri, 4 Oct 2024 10:09:54 +0300 Subject: [PATCH] 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' } });