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..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
@@ -26,8 +26,10 @@
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.function.Consumer;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@@ -51,6 +53,8 @@
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.VaadinSession;
+import elemental.json.Json;
+
/**
* Entry point for application classes hot reloads.
*
@@ -165,13 +169,46 @@ 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());
+
+ // Trigger any potential Flow translation updates
+ EnumMap> refreshActions = new EnumMap<>(
+ UIRefreshStrategy.class);
+ 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);
+ }
+
+ }
+
+ 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,
@@ -258,23 +295,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,
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..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
@@ -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' }
});