From a1cbb0d2eb94b63634063b23f0054299a2a9bf97 Mon Sep 17 00:00:00 2001 From: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:32:35 +0200 Subject: [PATCH 1/6] feat: Extend WebPushMessage with the custom settings (#20304) Add an API to set custom options as described in https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification#parameters Fixes #20285 --- .../com/vaadin/flow/server/frontend/sw.ts | 4 +- .../vaadin/flow/webpush/WebPushAction.java | 6 +++ .../vaadin/flow/webpush/WebPushOptions.java | 20 +++++++ .../com/vaadin/flow/webpush/WebPushView.java | 27 +++++++++- .../com/vaadin/flow/webpush/WebPushIT.java | 13 ++++- .../flow/server/webpush/WebPushMessage.java | 53 +++++++++++++++---- 6 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java create mode 100644 flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts index e40c8e972b4..e51c0f57df4 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts +++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/sw.ts @@ -149,9 +149,7 @@ self.addEventListener('message', (event) => { self.addEventListener('push', (e) => { const data = e.data?.json(); if (data) { - self.registration.showNotification(data.title, { - body: data.body, - }); + self.registration.showNotification(data.title, data.options); } }); diff --git a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java new file mode 100644 index 00000000000..4849a8eff27 --- /dev/null +++ b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushAction.java @@ -0,0 +1,6 @@ +package com.vaadin.flow.webpush; + +import java.io.Serializable; + +public record WebPushAction(String action, String title, String icon) implements Serializable { +} diff --git a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java new file mode 100644 index 00000000000..fdc0597eafd --- /dev/null +++ b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushOptions.java @@ -0,0 +1,20 @@ +package com.vaadin.flow.webpush; + +import java.io.Serializable; +import java.util.List; + +public record WebPushOptions(List actions, + String badge, + String body, + Serializable data, + String dir, + String icon, + String image, + String lang, + boolean renotify, + boolean requireInteraction, + boolean silent, + String tag, + long timestamp, + List vibrate) implements Serializable { +} diff --git a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java index bb72be89f9f..c9ae1667276 100644 --- a/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java +++ b/flow-tests/test-webpush/src/main/java/com/vaadin/flow/webpush/WebPushView.java @@ -16,6 +16,8 @@ package com.vaadin.flow.webpush; +import java.util.List; + import nl.martijndwars.webpush.Subscription; import com.vaadin.flow.component.Text; @@ -44,6 +46,12 @@ public class WebPushView extends Div { WebPush webPush; private final Div log; + private final WebPushAction webPushAction = new WebPushAction( + "dashboard", + "Open Dashboard", + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png" + ); + private Subscription subscription; public WebPushView() { @@ -73,8 +81,25 @@ public WebPushView() { notify = new NativeButton("Notify", event -> { if (subscription != null) { + WebPushOptions webPushOptions = new WebPushOptions( + List.of(webPushAction), + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png", + "Testing notification", + "This is my data!", + "rtl", + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png", + "https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png", + "de-DE", + true, + true, + false, + "My Notification", + System.currentTimeMillis(), + List.of(500, 500, 500) + ); + webPush.sendNotification(subscription, - new WebPushMessage(TEST_TITLE, "Testing notification")); + new WebPushMessage(TEST_TITLE, webPushOptions)); addLogEntry("Sent notification"); } else { addLogEntry("No notification sent due to missing subscription"); diff --git a/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java b/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java index feedcfc4768..03aeeb4bd24 100644 --- a/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java +++ b/flow-tests/test-webpush/src/test/java/com/vaadin/flow/webpush/WebPushIT.java @@ -153,7 +153,18 @@ public boolean isNotificationPresent(WebDriver driver) { .then( (notifications) => { return notifications.length == 1 && notifications[0].title === 'Test title' && - notifications[0].body === 'Testing notification'; + notifications[0].body === 'Testing notification' && + notifications[0].badge === 'https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png' && + notifications[0].data === 'This is my data!' && + notifications[0].dir === 'rtl' && + notifications[0].icon === 'https://upload.wikimedia.org/wikipedia/commons/0/0e/Message-icon-blue-symbol-double.png' && + notifications[0].lang === 'de-DE' && + notifications[0].renotify === true && + notifications[0].requireInteraction === true && + notifications[0].silent === false && + notifications[0].tag === 'My Notification' && + Array.isArray(notifications[0].actions) && notifications[0].actions.length > 0 && notifications[0].actions[0].action === 'dashboard' && + Array.isArray(notifications[0].vibrate) && notifications[0].vibrate.length > 0 && notifications[0].vibrate[0] === 500; }); """); } diff --git a/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java b/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java index 42ffbf2712a..01474ead43a 100644 --- a/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java +++ b/flow-webpush/src/main/java/com/vaadin/flow/server/webpush/WebPushMessage.java @@ -17,8 +17,8 @@ import java.io.Serializable; -import elemental.json.Json; -import elemental.json.JsonObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; /** * Web Push message object containing an information to be shown in the @@ -26,15 +26,40 @@ * * @since 24.2 */ -public record WebPushMessage(String title, String body) implements Serializable { +public record WebPushMessage(String title, ObjectNode options) implements Serializable { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Creates a new Web Push notification message with the specified title and various options + * fetched from a given Java object. + * + * @param title the notification title + * @param options any {@code Serializable} Java object representing custom settings to apply to the notification + * @see + * showNotification parameters + */ + public WebPushMessage(String title, Serializable options) { + this(title, objectMapper.convertValue(options, ObjectNode.class)); + } + + /** + * Creates a new Web Push notification message with just a title and body. + * + * @param title notification title + * @param body notification body + */ + public WebPushMessage(String title, String body) { + this(title, getBodyOption(body)); + } /** - * Creates a new Web Push notification message with title and body. + * Creates a new Web Push notification message with just a title. * * @param title notification title - * @param body notification body */ - public WebPushMessage { + public WebPushMessage(String title) { + this(title, (ObjectNode) null); } @Override @@ -48,9 +73,19 @@ public String toString() { * @return JSON representation of this message */ public String toJson() { - JsonObject json = Json.createObject(); + ObjectNode json = objectMapper.createObjectNode(); json.put("title", title); - json.put("body", body); - return json.toJson(); + if (options != null) { + json.set("options", options); + } + return json.toString(); + } + + private static ObjectNode getBodyOption(String body) { + ObjectNode objectNode = objectMapper.createObjectNode(); + if (body != null) { + objectNode.put("body", body); + } + return objectNode; } } From b72e39583cd836c2ef437249e20c4d3d4be86e4b Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 7 Nov 2024 10:07:50 +0100 Subject: [PATCH 2/6] fix: resume client to server communication after web socket reconnection (#20283) * fix: resume client to server communication after web socket reconnection When a websocket PUSH connection is closed and re-established because of a network failure, the RequestResponseTracker.hasActiveRequest is not reset, prenvint the Flow client to send additional messages to the server. This change will reset the flag on reconnection. It also will track unsent PUSH message over websocket, to retry the delivery once the connection is re-established, preventing client resynchronization. In addition, it sets a default value of 12 for the Atmospehere maxWebsocketErrorRetries setting, to ensure that the Flow client will attempt to reconnect with web socket transport several times, instead of immediately downgrade to long-polling after first failed connection. Fixes #20213 * upgrade to atmosphere javascript 4.0.1 with reconnection fixes --------- Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> --- .../AtmospherePushConnection.java | 1 + .../DefaultConnectionStateHandler.java | 12 ++++++++ .../client/communication/MessageSender.java | 29 ++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java b/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java index 84bd32fa212..c745b56592a 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/AtmospherePushConnection.java @@ -686,6 +686,7 @@ protected final native AtmosphereConfiguration createConfig() fallbackTransport: 'long-polling', contentType: 'application/json; charset=UTF-8', reconnectInterval: 5000, + maxWebsocketErrorRetries: 12, timeout: -1, maxReconnectOnClose: 10000000, trackMessageLength: true, diff --git a/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java b/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java index b918e60eb72..b023ecca5ee 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java @@ -458,6 +458,18 @@ public void pushOk(PushConnection pushConnection) { debug("pushOk()"); if (isReconnecting()) { resolveTemporaryError(Type.PUSH); + if (registry.getRequestResponseTracker().hasActiveRequest()) { + debug("pushOk() Reset active request state when reconnecting PUSH because of a network error."); + endRequest(); + // for bidirectional transport, the pending message is not sent + // as reconnection payload, so immediately push the pending + // changes on reconnect + if (pushConnection.isBidirectional()) { + Console.debug( + "Flush pending messages after PUSH reconnection."); + registry.getMessageSender().sendInvocationsToServer(); + } + } } } diff --git a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java index 80480c2dd4e..80e65ecf702 100644 --- a/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java +++ b/flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java @@ -64,6 +64,8 @@ public enum ResynchronizationState { private ResynchronizationState resynchronizationState = ResynchronizationState.NOT_ACTIVE; + private JsonObject pushPendingMessage; + /** * Creates a new instance connected to the given registry. * @@ -104,6 +106,17 @@ public void sendInvocationsToServer() { * */ private void doSendInvocationsToServer() { + // If there's a stored message, resend it and postpone processing the + // rest of the queued messages to prevent resynchronization issues. + if (pushPendingMessage != null) { + Console.log("Sending pending push message " + + pushPendingMessage.toJson()); + JsonObject payload = pushPendingMessage; + pushPendingMessage = null; + registry.getRequestResponseTracker().startRequest(); + send(payload); + return; + } ServerRpcQueue serverRpcQueue = registry.getServerRpcQueue(); if (serverRpcQueue.isEmpty() @@ -181,6 +194,13 @@ private JsonObject preparePayload(final JsonArray reqInvocations, */ public void send(final JsonObject payload) { if (push != null && push.isBidirectional()) { + // When using bidirectional transport, the payload is not resent + // to the server during reconnection attempts. + // Keep a copy of the message, so that it could be resent to the + // server after a reconnection. + // Reference will be cleaned up once the server confirms it has + // seen this message + pushPendingMessage = payload; push.push(payload); } else { registry.getXhrConnection().send(payload); @@ -260,7 +280,14 @@ public void resynchronize() { */ public void setClientToServerMessageId(int nextExpectedId, boolean force) { if (nextExpectedId == clientToServerMessageId) { - // No op as everything matches they way it should + // Everything matches they way it should + // Remove potential pending PUSH message if it has already been seen + // by the server. + if (pushPendingMessage != null + && (int) pushPendingMessage.getNumber( + ApplicationConstants.CLIENT_TO_SERVER_ID) < nextExpectedId) { + pushPendingMessage = null; + } return; } if (force) { From a2fe8b6ce63439ce7db9359c62539862034863f8 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 7 Nov 2024 12:04:58 +0100 Subject: [PATCH 3/6] fix: support serialization of Vaadin scoped beans (#20394) Makes sure that VaadinSession and UI thread locals are available during both serialization and deserialization, to allow other libraries to perform inspection and injection of Vaadin scoped beans. Also refactors VaadinRouteScope to be independent from VaadinService when fetching RouteScopeOwner annotation for the bean, and replaces VaadinSession.unlock() calls with direct access to the lock instance to prevent unwanted push during bean lookup. Fixes #19967 Part of vaadin/kubernetes-kit#140 Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com> --- .../com/vaadin/flow/component/Component.java | 40 +++++ .../com/vaadin/flow/server/VaadinSession.java | 53 ++++-- .../tests/server/SerializationTest.java | 167 +++++++++++++++++- .../vaadin/flow/spring/scopes/BeanStore.java | 4 +- .../flow/spring/scopes/VaadinRouteScope.java | 95 ++++++---- .../spring/scopes/VaadinSessionScope.java | 5 +- .../flow/spring/scopes/VaadinUIScope.java | 4 +- .../spring/SpringClassesSerializableTest.java | 1 + .../flow/spring/scopes/AbstractScopeTest.java | 2 + .../spring/scopes/VaadinRouteScopeTest.java | 15 +- 10 files changed, 317 insertions(+), 69 deletions(-) diff --git a/flow-server/src/main/java/com/vaadin/flow/component/Component.java b/flow-server/src/main/java/com/vaadin/flow/component/Component.java index a68d6895e8d..854a7ea97eb 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/Component.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/Component.java @@ -15,10 +15,15 @@ */ package com.vaadin.flow.component; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serial; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import java.util.stream.Stream.Builder; @@ -32,6 +37,7 @@ import com.vaadin.flow.dom.ShadowRoot; import com.vaadin.flow.i18n.I18NProvider; import com.vaadin.flow.internal.AnnotationReader; +import com.vaadin.flow.internal.CurrentInstance; import com.vaadin.flow.internal.LocaleUtil; import com.vaadin.flow.internal.nodefeature.ElementData; import com.vaadin.flow.server.Attributes; @@ -820,4 +826,38 @@ public void removeFromParent() { getElement().removeFromParent(); } + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + if (this instanceof UI ui) { + Map, CurrentInstance> instances = CurrentInstance + .setCurrent(ui); + try { + out.defaultWriteObject(); + } finally { + CurrentInstance.restoreInstances(instances); + } + } else { + out.defaultWriteObject(); + } + } + + @Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + if (this instanceof UI ui) { + Map, CurrentInstance> instances = CurrentInstance + .getInstances(); + // Cannot use CurrentInstance.setCurrent(this) because it will try + // to get VaadinSession from UI.internals that is not yet available + CurrentInstance.set(UI.class, ui); + try { + in.defaultReadObject(); + } finally { + CurrentInstance.restoreInstances(instances); + } + } else { + in.defaultReadObject(); + } + } + } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java index b1170eb84ff..09dacdb03d8 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/VaadinSession.java @@ -19,6 +19,7 @@ import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSessionBindingEvent; import jakarta.servlet.http.HttpSessionBindingListener; + import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; @@ -1097,6 +1098,14 @@ private void readObject(ObjectInputStream stream) Map, CurrentInstance> old = CurrentInstance.setCurrent(this); try { stream.defaultReadObject(); + // Add-ons may have Listener classes that nullify themselves during + // serialization (e.g. Collaboration Kit) and restore instances in + // some custom way later on. + // Removing null elements prevents application to fail if restore + // actions are not applied eagerly + requestHandlers.remove(null); + destroyListeners.remove(null); + uIs = (Map) stream.readObject(); resourceRegistry = (StreamResourceRegistry) stream.readObject(); pendingAccessQueue = new ConcurrentLinkedQueue<>(); @@ -1107,27 +1116,33 @@ private void readObject(ObjectInputStream stream) private void writeObject(java.io.ObjectOutputStream stream) throws IOException { - boolean serializeUIs = true; - - // If service is null it has just been deserialized and should be - // serialized in - // the same way again - if (getService() != null) { - ApplicationConfiguration appConfiguration = ApplicationConfiguration - .get(getService().getContext()); - if (!appConfiguration.isProductionMode() && !appConfiguration - .isDevModeSessionSerializationEnabled()) { - serializeUIs = false; + Map, CurrentInstance> instanceMap = CurrentInstance + .setCurrent(this); + try { + boolean serializeUIs = true; + + // If service is null it has just been deserialized and should be + // serialized in + // the same way again + if (getService() != null) { + ApplicationConfiguration appConfiguration = ApplicationConfiguration + .get(getService().getContext()); + if (!appConfiguration.isProductionMode() && !appConfiguration + .isDevModeSessionSerializationEnabled()) { + serializeUIs = false; + } } - } - stream.defaultWriteObject(); - if (serializeUIs) { - stream.writeObject(uIs); - stream.writeObject(resourceRegistry); - } else { - stream.writeObject(new HashMap<>()); - stream.writeObject(new StreamResourceRegistry(this)); + stream.defaultWriteObject(); + if (serializeUIs) { + stream.writeObject(uIs); + stream.writeObject(resourceRegistry); + } else { + stream.writeObject(new HashMap<>()); + stream.writeObject(new StreamResourceRegistry(this)); + } + } finally { + CurrentInstance.restoreInstances(instanceMap); } } diff --git a/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java b/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java index 57be740c9e1..e646a5d3b3f 100644 --- a/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java +++ b/flow-server/src/test/java/com/vaadin/tests/server/SerializationTest.java @@ -5,19 +5,29 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.Serial; import java.io.Serializable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.UI; import com.vaadin.flow.function.DeploymentConfiguration; +import com.vaadin.flow.server.RequestHandler; import com.vaadin.flow.server.StreamRegistration; import com.vaadin.flow.server.StreamResource; import com.vaadin.flow.server.VaadinContext; import com.vaadin.flow.server.VaadinRequest; +import com.vaadin.flow.server.VaadinResponse; import com.vaadin.flow.server.VaadinService; import com.vaadin.flow.server.VaadinServletService; import com.vaadin.flow.server.VaadinSession; @@ -29,6 +39,30 @@ public class SerializationTest { + Runnable cleaner; + + @Before + public void enabledSerializationDebugInfo() { + String extendedDebugInfo = System + .getProperty("sun.io.serialization.extendedDebugInfo"); + System.setProperty("sun.io.serialization.extendedDebugInfo", "true"); + cleaner = () -> { + if (extendedDebugInfo != null) { + System.setProperty("sun.io.serialization.extendedDebugInfo", + extendedDebugInfo); + } else { + System.clearProperty("sun.io.serialization.extendedDebugInfo"); + } + }; + } + + @After + public void restore() { + if (cleaner != null) { + cleaner.run(); + } + } + @Test public void testSerializeVaadinSession_accessQueueIsRecreated() throws Exception { @@ -123,8 +157,119 @@ public void testSerializeVaadinSession_notProductionMode_canSerializeWithoutTran Assert.assertNull(againSerializedAndDeserializedSession.getService()); } + @Test + // Covers serialization of UI scoped beans, e.g. in Kubernetes Kit + // https://github.com/vaadin/flow/issues/19967 + // https://github.com/vaadin/kubernetes-kit/issues/140 + public void serializeUI_currentUI_availableDuringSerialization() + throws Exception { + VaadinSession deserializeSession = serializeAndDeserializeWithUI(true, + true, ui -> ui.add(new MyComponent())); + MyComponent deserializedComponent = deserializeSession.getUIs() + .iterator().next().getChildren() + .filter(MyComponent.class::isInstance) + .map(MyComponent.class::cast).findFirst() + .orElseThrow(() -> new AssertionError( + "Custom component has not been deserialized")); + + deserializedComponent.checker.assertInstancesAvailable(); + } + + @Test + // Covers serialization of UI scoped beans, e.g. in Kubernetes Kit + // https://github.com/vaadin/flow/issues/19967 + // https://github.com/vaadin/kubernetes-kit/issues/140 + public void serializeUI_currentVaadinSession_availableDuringSerialization() + throws Exception { + VaadinSession deserializeSession = serializeAndDeserializeWithUI(true, + true, + ui -> ui.getSession().addRequestHandler(new MyListener())); + + MyListener deserializedListener = deserializeSession + .getRequestHandlers().stream() + .filter(MyListener.class::isInstance) + .map(MyListener.class::cast).findFirst() + .orElseThrow(() -> new AssertionError( + "Session request listener has not been deserialized")); + + deserializedListener.checker.assertSessionAvailable(); + } + + private static class SerializationInstancesChecker implements Serializable { + private boolean uiAvailableOnRead = false; + private boolean sessionAvailableOnRead = false; + private boolean uiAvailableOnWrite = false; + private boolean sessionAvailableOnWrite = false; + + @Serial + private void writeObject(ObjectOutputStream out) throws IOException { + uiAvailableOnWrite = UI.getCurrent() != null; + sessionAvailableOnWrite = VaadinSession.getCurrent() != null; + out.defaultWriteObject(); + } + + @Serial + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + uiAvailableOnRead = UI.getCurrent() != null; + sessionAvailableOnRead = VaadinSession.getCurrent() != null; + } + + void assertInstancesAvailable() { + assertUIAvailable(); + assertSessionAvailable(); + } + + void assertUIAvailable() { + Assert.assertTrue( + "Expecting serialization hook to be called with UI thread local set", + uiAvailableOnWrite); + Assert.assertTrue( + "Expecting deserialization hook to be called with UI thread local set", + uiAvailableOnRead); + } + + void assertSessionAvailable() { + Assert.assertTrue( + "Expecting serialization hook to be called with VaadinSession thread local set", + sessionAvailableOnWrite); + Assert.assertTrue( + "Expecting deserialization hook to be called with VaadinSession thread local set", + sessionAvailableOnRead); + } + + } + + @Tag("my-component") + private static class MyComponent extends Component { + + private final SerializationInstancesChecker checker = new SerializationInstancesChecker(); + + } + + private static class MyListener implements RequestHandler { + + private final SerializationInstancesChecker checker = new SerializationInstancesChecker(); + + @Override + public boolean handleRequest(VaadinSession session, + VaadinRequest request, VaadinResponse response) + throws IOException { + return false; + } + } + + private static VaadinSession serializeAndDeserializeWithUI( + boolean serializeUI) throws Exception { + return serializeAndDeserializeWithUI(serializeUI, false, ui -> { + }); + } + private static VaadinSession serializeAndDeserializeWithUI( - boolean serializeUI) throws IOException, ClassNotFoundException { + boolean serializeUI, boolean background, Consumer uiConsumer) + throws Exception { + VaadinService vaadinService = new MockVaadinService(false, serializeUI); VaadinSession session = new VaadinSession(vaadinService); // This is done only for test purpose to init the session lock, @@ -136,13 +281,25 @@ private static VaadinSession serializeAndDeserializeWithUI( MockUI ui = new MockUI(session); ui.doInit(null, 42); session.addUI(ui); - - session = serializeAndDeserialize(session); + uiConsumer.accept(ui); + + VaadinSession deserializedSession; + if (background) { + deserializedSession = CompletableFuture.supplyAsync(() -> { + try { + return serializeAndDeserialize(session); + } catch (Exception e) { + throw new RuntimeException(e); + } + }).get(); + } else { + deserializedSession = serializeAndDeserialize(session); + } // This is done only for test purpose to refresh the session lock, // should be called by Flow internally as soon as the session has // been retrieved from http session. - session.refreshTransients(null, vaadinService); - return session; + deserializedSession.refreshTransients(null, vaadinService); + return deserializedSession; } private static S serializeAndDeserialize(S s) diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java index 74e1c1d3c7a..fe077909642 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/BeanStore.java @@ -149,11 +149,11 @@ private T execute(Supplier supplier) { if (session.hasLock()) { return supplier.get(); } else { - session.lock(); + session.getLockInstance().lock(); try { return supplier.get(); } finally { - session.unlock(); + session.getLockInstance().unlock(); } } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java index c179f339df8..ae57e1bcb58 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinRouteScope.java @@ -15,8 +15,6 @@ */ package com.vaadin.flow.spring.scopes; -import jakarta.servlet.ServletContext; - import java.io.Serializable; import java.util.Collections; import java.util.HashMap; @@ -26,11 +24,10 @@ import java.util.Set; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.context.ApplicationContext; import org.springframework.lang.NonNull; -import org.springframework.web.context.support.WebApplicationContextUtils; import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.ComponentUtil; @@ -45,9 +42,6 @@ import com.vaadin.flow.router.RouterLayout; import com.vaadin.flow.server.UIInitEvent; import com.vaadin.flow.server.UIInitListener; -import com.vaadin.flow.server.VaadinContext; -import com.vaadin.flow.server.VaadinService; -import com.vaadin.flow.server.VaadinServletContext; import com.vaadin.flow.server.VaadinSession; import com.vaadin.flow.shared.Registration; import com.vaadin.flow.spring.annotation.RouteScopeOwner; @@ -63,7 +57,7 @@ * @since * */ -public class VaadinRouteScope extends AbstractScope implements UIInitListener { +public class VaadinRouteScope extends AbstractScope { public static final String VAADIN_ROUTE_SCOPE_NAME = "vaadin-route"; @@ -141,7 +135,7 @@ private void destroy() { } - private class NavigationListener + private static class NavigationListener implements BeforeEnterListener, AfterNavigationListener, ComponentEventListener, Serializable { @@ -315,8 +309,8 @@ public void onComponentEvent(DetachEvent event) { @Override protected Object doGet(String name, ObjectFactory objectFactory) { - RouteScopeOwner owner = getContext().findAnnotationOnBean(name, - RouteScopeOwner.class); + RouteScopeObjectFactory cast = (RouteScopeObjectFactory) objectFactory; + RouteScopeOwner owner = cast.getOwner(); if (!getNavigationListener().hasNavigationOwner(owner)) { assert owner != null; throw new IllegalStateException(String.format( @@ -324,15 +318,18 @@ protected Object doGet(String name, ObjectFactory objectFactory) { + "active navigation components chain: the scope defined by the bean '%s' doesn't exist.", owner.value(), name)); } - return super.doGet(name, objectFactory); + Object object = super.doGet(name, objectFactory); + if (object instanceof ObjectWithOwner wrapper) { + return wrapper.object; + } + return object; } @Override protected void storeBean(String name, Object bean) { - super.storeBean(name, bean); - RouteScopeOwner owner = getContext().findAnnotationOnBean(name, - RouteScopeOwner.class); - getNavigationListener().storeOwner(name, owner); + ObjectWithOwner wrapper = (ObjectWithOwner) bean; + super.storeBean(name, wrapper.object); + getNavigationListener().storeOwner(name, wrapper.owner); } BeanNamesWrapper getBeanNamesWrapper() { @@ -348,17 +345,6 @@ private boolean resetUI() { return true; } - @NonNull - private ApplicationContext getContext() { - VaadinService service = currentUI.getSession().getService(); - VaadinContext context = service.getContext(); - ServletContext servletContext = ((VaadinServletContext) context) - .getContext(); - assert servletContext != null; - return WebApplicationContextUtils - .getRequiredWebApplicationContext(servletContext); - } - @NonNull private NavigationListener getNavigationListener() { NavigationListener navigationListener = ComponentUtil @@ -369,10 +355,52 @@ private NavigationListener getNavigationListener() { } + private record ObjectWithOwner(Object object, RouteScopeOwner owner) { + } + + private static class RouteScopeObjectFactory + implements ObjectFactory { + + private final ObjectFactory objectFactory; + private final RouteScopeOwner owner; + + public RouteScopeObjectFactory(ObjectFactory objectFactory, + RouteScopeOwner owner) { + this.objectFactory = objectFactory; + this.owner = owner; + } + + @Override + public ObjectWithOwner getObject() throws BeansException { + return new ObjectWithOwner(objectFactory.getObject(), owner); + } + + public RouteScopeOwner getOwner() { + return owner; + } + } + + static class NavigationListenerRegistrar implements UIInitListener { + + @Override + public void uiInit(UIInitEvent event) { + NavigationListener listener = new NavigationListener(event.getUI()); + ComponentUtil.setData(event.getUI(), NavigationListener.class, + listener); + } + + } + + private ConfigurableListableBeanFactory beanFactory; + @Override public void postProcessBeanFactory( ConfigurableListableBeanFactory beanFactory) { beanFactory.registerScope(VAADIN_ROUTE_SCOPE_NAME, this); + beanFactory.registerSingleton( + NavigationListenerRegistrar.class.getName(), + new NavigationListenerRegistrar()); + this.beanFactory = beanFactory; } @Override @@ -381,16 +409,15 @@ public String getConversationId() { } @Override - public void uiInit(UIInitEvent event) { - NavigationListener listener = new NavigationListener(event.getUI()); - ComponentUtil.setData(event.getUI(), NavigationListener.class, - listener); + public Object get(String name, ObjectFactory objectFactory) { + return super.get(name, new RouteScopeObjectFactory(objectFactory, + beanFactory.findAnnotationOnBean(name, RouteScopeOwner.class))); } @Override protected BeanStore getBeanStore() { final VaadinSession session = getVaadinSession(); - session.lock(); + session.getLockInstance().lock(); try { BeanStore store = getBeanStoreIfExists(session); if (store == null) { @@ -400,11 +427,11 @@ protected BeanStore getBeanStore() { } return store; } finally { - session.unlock(); + session.getLockInstance().unlock(); } } - private RouteBeanStore getBeanStoreIfExists(VaadinSession session) { + private static RouteBeanStore getBeanStoreIfExists(VaadinSession session) { assert session.hasLock(); RouteStoreWrapper wrapper = session .getAttribute(RouteStoreWrapper.class); diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java index a4893aaa204..5d5aa11e602 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinSessionScope.java @@ -19,7 +19,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import com.vaadin.flow.server.VaadinSession; -import com.vaadin.flow.shared.Registration; /** * Implementation of Spring's @@ -67,7 +66,7 @@ public String getConversationId() { @Override protected BeanStore getBeanStore() { final VaadinSession session = getVaadinSession(); - session.lock(); + session.getLockInstance().lock(); try { BeanStore beanStore = session.getAttribute(BeanStore.class); if (beanStore == null) { @@ -76,7 +75,7 @@ protected BeanStore getBeanStore() { } return beanStore; } finally { - session.unlock(); + session.getLockInstance().unlock(); } } diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java index bfc11b79203..9e86020f460 100644 --- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java +++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/scopes/VaadinUIScope.java @@ -108,7 +108,7 @@ public String getConversationId() { @Override protected BeanStore getBeanStore() { final VaadinSession session = getVaadinSession(); - session.lock(); + session.getLockInstance().lock(); try { UIStoreWrapper wrapper = session.getAttribute(UIStoreWrapper.class); if (wrapper == null) { @@ -117,7 +117,7 @@ protected BeanStore getBeanStore() { } return wrapper.getBeanStore(getUI()); } finally { - session.unlock(); + session.getLockInstance().unlock(); } } diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java index a0eb5d846d9..886073b9e75 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java @@ -97,6 +97,7 @@ protected Stream getExcludedPatterns() { "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinBeanFactoryInitializationAotProcessor\\$Marker", "com\\.vaadin\\.flow\\.spring\\.springnative\\.VaadinHintsRegistrar", + "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinRouteScope(\\$.*)?", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinSessionScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope", "com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope", diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java index 75d91b6cfaf..7622f9000d7 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/AbstractScopeTest.java @@ -180,6 +180,8 @@ protected VaadinSession mockSession() { when(session.getConfiguration()).thenReturn(config); VaadinSession.setCurrent(session); + ReentrantLock lock = new ReentrantLock(); + when(session.getLockInstance()).thenReturn(lock); when(session.hasLock()).thenReturn(true); // keep a reference to the session so that it cannot be GCed. diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java index 3ed0babf774..d124ea822ac 100644 --- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java +++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/scopes/VaadinRouteScopeTest.java @@ -25,6 +25,7 @@ import org.junit.Test; import org.mockito.Mockito; import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.web.context.WebApplicationContext; import com.vaadin.flow.component.Component; @@ -57,7 +58,10 @@ public static class AnotherNavigationTarget extends Component { @Override protected VaadinRouteScope getScope() { - return new VaadinRouteScope(); + VaadinRouteScope scope = new VaadinRouteScope(); + scope.postProcessBeanFactory( + Mockito.mock(ConfigurableListableBeanFactory.class)); + return scope; } @Test @@ -148,7 +152,8 @@ public void refresh_uiWithTheSameWindowName_beanInScopeIsDestroyedAfterRefresh() AtomicInteger count = new AtomicInteger(); scope.registerDestructionCallback("foo", () -> count.getAndIncrement()); - scope.uiInit(new UIInitEvent(ui, ui.getSession().getService())); + new VaadinRouteScope.NavigationListenerRegistrar() + .uiInit(new UIInitEvent(ui, ui.getSession().getService())); navigateTo(ui, new NavigationTarget()); @@ -195,7 +200,8 @@ public void detachUI_uiWithDifferentWindowName_beanInScopeIsDestroyedwhenUIIsDet AtomicInteger count = new AtomicInteger(); scope.registerDestructionCallback("foo", () -> count.getAndIncrement()); - scope.uiInit(new UIInitEvent(ui, ui.getSession().getService())); + new VaadinRouteScope.NavigationListenerRegistrar() + .uiInit(new UIInitEvent(ui, ui.getSession().getService())); navigateTo(ui, new NavigationTarget()); @@ -243,7 +249,8 @@ private VaadinRouteScope initScope(UI ui) { VaadinRouteScope scope = getScope(); scope.getBeanStore(); - scope.uiInit(new UIInitEvent(ui, ui.getSession().getService())); + new VaadinRouteScope.NavigationListenerRegistrar() + .uiInit(new UIInitEvent(ui, ui.getSession().getService())); return scope; } From 01a6eecc661964153b4beae0970602e3e8dc6a95 Mon Sep 17 00:00:00 2001 From: Marco Collovati Date: Thu, 7 Nov 2024 12:49:48 +0100 Subject: [PATCH 4/6] test: prevent closing Vite websocket client if already closed by server (#20425) --- .../devserver/viteproxy/ViteWebsocketConnectionTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java index 5b4aa8eff9b..3d3da7bb10e 100644 --- a/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java +++ b/vaadin-dev-server/src/test/java/com/vaadin/base/devserver/viteproxy/ViteWebsocketConnectionTest.java @@ -91,8 +91,10 @@ public void onOpen(WebSocket webSocket) { "Should not have been blocked too long after connection (elapsed time: " + elapsedTime + ")", elapsedTime < 1000); - viteConnection.close(); - closeLatch.await(2, TimeUnit.SECONDS); + if (!closeLatch.await(500, TimeUnit.MILLISECONDS)) { + viteConnection.close(); + closeLatch.await(500, TimeUnit.MILLISECONDS); + } } @Test From fd16ca62c21b6387620bdcc8159ca55417ecbe01 Mon Sep 17 00:00:00 2001 From: Tomi Virtanen Date: Thu, 7 Nov 2024 15:01:17 +0200 Subject: [PATCH 5/6] feat: opt-out web components from package.json (#20392) Adds new property `npm.excludeWebComponents` (or `npmExcludeWebComponents` in Maven configurations). By default, it's `false` and everything works as before. `true` will exclude all web component dependencies from `package.json` for development mode (Vite/dev bundle) and production bundle build. Excluded dependencies are all Vaadin core components (e.g. button, grid, login, etc.) and commercial components (e.g. charts, rich-text-editor, etc.), but not lumo/material themes. RelatedTo: #19948 --------- Co-authored-by: caalador --- .../flow/plugin/maven/BuildDevBundleMojo.java | 8 + .../com/vaadin/gradle/GradlePluginAdapter.kt | 2 + .../gradle/PrepareFrontendInputProperties.kt | 3 + .../gradle/VaadinFlowPluginExtension.kt | 8 +- .../plugin/maven/FlowModeAbstractMojo.java | 8 + .../flow/plugin/maven/GenerateNpmBOMMojo.java | 4 +- .../flow/plugin/base/BuildFrontendUtil.java | 18 +- .../flow/plugin/base/PluginAdapterBase.java | 7 + .../vaadin/flow/server/InitParameters.java | 5 + .../server/frontend/BundleValidationUtil.java | 3 +- .../flow/server/frontend/ExclusionFilter.java | 20 ++- .../flow/server/frontend/NodeUpdater.java | 7 +- .../vaadin/flow/server/frontend/Options.java | 23 +++ .../server/frontend/TaskUpdatePackages.java | 3 +- .../frontend/VersionsJsonConverter.java | 32 +++- .../startup/AbstractConfigurationFactory.java | 6 + .../flow/server/frontend/NodeUpdaterTest.java | 50 +++++- .../frontend/TaskUpdatePackagesNpmTest.java | 160 +++++++++++++++++- .../frontend/VersionsJsonConverterTest.java | 51 +++++- .../devserver/startup/DevModeInitializer.java | 6 +- 20 files changed, 394 insertions(+), 30 deletions(-) diff --git a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java index b6562f494ab..6248509201f 100644 --- a/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java +++ b/flow-plugins/flow-dev-bundle-plugin/src/main/java/com/vaadin/flow/plugin/maven/BuildDevBundleMojo.java @@ -171,6 +171,9 @@ public class BuildDevBundleMojo extends AbstractMojo @Parameter(defaultValue = "${project.basedir}/src/main/" + FRONTEND) private File frontendDirectory; + @Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false") + private boolean npmExcludeWebComponents; + @Override public void execute() throws MojoFailureException { long start = System.nanoTime(); @@ -469,4 +472,9 @@ public boolean checkRuntimeDependency(String groupId, String artifactId, Consumer missingDependencyMessageConsumer) { return false; } + + @Override + public boolean isNpmExcludeWebComponents() { + return npmExcludeWebComponents; + } } diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt index 977572330ad..84ea3e5add4 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/GradlePluginAdapter.kt @@ -218,6 +218,8 @@ internal class GradlePluginAdapter( override fun applicationIdentifier(): String = config.applicationIdentifier.get() + override fun isNpmExcludeWebComponents(): Boolean = config.npmExcludeWebComponents.get() + override fun checkRuntimeDependency( groupId: String, artifactId: String, diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt index 79498ac60de..f5467c90f2e 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/PrepareFrontendInputProperties.kt @@ -142,6 +142,9 @@ internal class PrepareFrontendInputProperties(private val config: PluginEffectiv @Input public fun getApplicationIdentifier(): Provider = config.applicationIdentifier + @Input + public fun getNpmExcludeWebComponents(): Provider = config.npmExcludeWebComponents + @Input @Optional public fun getNodeExecutablePath(): Provider = tools diff --git a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt index 0924369e56c..1dfcf839177 100644 --- a/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt +++ b/flow-plugins/flow-gradle-plugin/src/main/kotlin/com/vaadin/gradle/VaadinFlowPluginExtension.kt @@ -282,6 +282,8 @@ public abstract class VaadinFlowPluginExtension @Inject constructor(private val public abstract val applicationIdentifier: Property + public abstract val npmExcludeWebComponents: Property + public fun filterClasspath(@DelegatesTo(value = ClasspathFilter::class, strategy = Closure.DELEGATE_FIRST) block: Closure<*>) { block.delegate = classpathFilter block.resolveStrategy = Closure.DELEGATE_FIRST @@ -439,6 +441,9 @@ public class PluginEffectiveConfiguration( )) .overrideWithSystemProperty("vaadin.${InitParameters.APPLICATION_IDENTIFIER}") + public val npmExcludeWebComponents: Provider = extension + .npmExcludeWebComponents.convention(false) + /** * Finds the value of a boolean property. It searches in gradle and system properties. * @@ -499,7 +504,8 @@ public class PluginEffectiveConfiguration( "alwaysExecutePrepareFrontend=${alwaysExecutePrepareFrontend.get()}, " + "frontendHotdeploy=${frontendHotdeploy.get()}," + "reactEnable=${reactEnable.get()}," + - "cleanFrontendFiles=${cleanFrontendFiles.get()}" + + "cleanFrontendFiles=${cleanFrontendFiles.get()}," + + "npmExcludeWebComponents=${npmExcludeWebComponents.get()}" + ")" public companion object { public fun get(project: Project): PluginEffectiveConfiguration = diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java index ae3b389c61f..62bc3d37fc0 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/FlowModeAbstractMojo.java @@ -234,6 +234,9 @@ public abstract class FlowModeAbstractMojo extends AbstractMojo @Parameter(property = InitParameters.REACT_ENABLE, defaultValue = "${null}") private Boolean reactEnable; + @Parameter(property = InitParameters.NPM_EXCLUDE_WEB_COMPONENTS, defaultValue = "false") + private boolean npmExcludeWebComponents; + /** * Identifier for the application. *

@@ -570,4 +573,9 @@ public String applicationIdentifier() { project.getGroupId() + ":" + project.getArtifactId(), StandardCharsets.UTF_8); } + + @Override + public boolean isNpmExcludeWebComponents() { + return npmExcludeWebComponents; + } } diff --git a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java index 479e6cbbf48..5926cc68f68 100644 --- a/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java +++ b/flow-plugins/flow-maven-plugin/src/main/java/com/vaadin/flow/plugin/maven/GenerateNpmBOMMojo.java @@ -176,7 +176,9 @@ public void execute() throws MojoExecutionException, MojoFailureException { .withHomeNodeExecRequired(requireHomeNodeExec()) .setJavaResourceFolder(javaResourceFolder()) .withProductionMode(productionMode) - .withReact(isReactEnabled()); + .withReact(isReactEnabled()) + .withNpmExcludeWebComponents( + isNpmExcludeWebComponents()); new NodeTasks(options).execute(); logInfo("SBOM generation created node_modules and all needed metadata. " + "If you don't need it, please run mvn vaadin:clean-frontend"); diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java index afd1511c7cb..dd8c5679207 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java @@ -77,6 +77,7 @@ import static com.vaadin.flow.server.InitParameters.FRONTEND_HOTDEPLOY; import static com.vaadin.flow.server.InitParameters.NODE_DOWNLOAD_ROOT; import static com.vaadin.flow.server.InitParameters.NODE_VERSION; +import static com.vaadin.flow.server.InitParameters.NPM_EXCLUDE_WEB_COMPONENTS; import static com.vaadin.flow.server.InitParameters.REACT_ENABLE; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_INITIAL_UIDL; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_PRODUCTION_MODE; @@ -165,7 +166,9 @@ public static void prepareFrontend(PluginAdapterBase adapter) .setNodeAutoUpdate(adapter.nodeAutoUpdate()) .withHomeNodeExecRequired(adapter.requireHomeNodeExec()) .setJavaResourceFolder(adapter.javaResourceFolder()) - .withProductionMode(false).withReact(adapter.isReactEnabled()); + .withProductionMode(false).withReact(adapter.isReactEnabled()) + .withNpmExcludeWebComponents( + adapter.isNpmExcludeWebComponents()); // Copy jar artifact contents in TaskCopyFrontendFiles options.copyResources(adapter.getJarFiles()); @@ -263,6 +266,10 @@ public static File propagateBuildInfo(PluginAdapterBase adapter) { } buildInfo.put(REACT_ENABLE, adapter.isReactEnabled()); + if (adapter.isNpmExcludeWebComponents()) { + buildInfo.put(NPM_EXCLUDE_WEB_COMPONENTS, + adapter.isNpmExcludeWebComponents()); + } try { FileUtils.forceMkdir(token.getParentFile()); @@ -339,7 +346,9 @@ public static void runNodeUpdater(PluginAdapterBuild adapter) .withPostinstallPackages(adapter.postinstallPackages()) .withCiBuild(adapter.ciBuild()) .withForceProductionBuild(adapter.forceProductionBuild()) - .withReact(adapter.isReactEnabled()); + .withReact(adapter.isReactEnabled()) + .withNpmExcludeWebComponents( + adapter.isNpmExcludeWebComponents()); new NodeTasks(options).execute(); } catch (ExecutionFailedException exception) { throw exception; @@ -405,7 +414,9 @@ public static void runDevBuildNodeUpdater(PluginAdapterBuild adapter) .withBundleBuild(true) .skipDevBundleBuild(adapter.skipDevBundleBuild()) .withCompressBundle(adapter.compressBundle()) - .withReact(adapter.isReactEnabled()); + .withReact(adapter.isReactEnabled()) + .withNpmExcludeWebComponents( + adapter.isNpmExcludeWebComponents()); new NodeTasks(options).execute(); } catch (ExecutionFailedException exception) { throw exception; @@ -751,6 +762,7 @@ public static void updateBuildFile(PluginAdapterBuild adapter, buildInfo.remove(Constants.CONNECT_OPEN_API_FILE_TOKEN); buildInfo.remove(Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN); buildInfo.remove(InitParameters.BUILD_FOLDER); + buildInfo.remove(InitParameters.NPM_EXCLUDE_WEB_COMPONENTS); // Premium features flag is always true, because Vaadin CI server // uses Enterprise sub, thus it's always true. // Thus, resets the premium feature flag and DAU flag before asking diff --git a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java index 2a3186651ad..4c431d0b4a7 100644 --- a/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java +++ b/flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/PluginAdapterBase.java @@ -337,4 +337,11 @@ default Lookup createLookup(ClassFinder classFinder) { * {@literal blank}. */ String applicationIdentifier(); + + /** + * Whether to include web component npm packages in packages.json. + * + * @return {@code true} to include web component npm packages. + */ + boolean isNpmExcludeWebComponents(); } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java b/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java index a9889197a45..6f0a8df3bc0 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/InitParameters.java @@ -285,4 +285,9 @@ public class InitParameters implements Serializable { */ public static final String APPLICATION_IDENTIFIER = "applicationIdentifier"; + /** + * Configuration name for excluding npm packages for web components. + */ + public static final String NPM_EXCLUDE_WEB_COMPONENTS = "npm.excludeWebComponents"; + } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java index 4c7a335a3b3..ac1542d1fc7 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/BundleValidationUtil.java @@ -316,7 +316,8 @@ public void execute() { Map filteredApplicationDependencies = new ExclusionFilter( options.getClassFinder(), options.isReactEnabled() - && FrontendUtils.isReactModuleAvailable(options)) + && FrontendUtils.isReactModuleAvailable(options), + options.isNpmExcludeWebComponents()) .exclude(applicationDependencies); // Add application dependencies diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java index 361fe37402b..5ae30bb629f 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/ExclusionFilter.java @@ -43,6 +43,8 @@ public class ExclusionFilter implements Serializable { private final boolean reactEnabled; + private final boolean excludeWebComponentNpmPackages; + /** * Create a new exclusion filter. * @@ -52,8 +54,24 @@ public class ExclusionFilter implements Serializable { * whether React is enabled */ public ExclusionFilter(ClassFinder finder, boolean reactEnabled) { + this(finder, reactEnabled, false); + } + + /** + * Create a new exclusion filter. + * + * @param finder + * the class finder to use + * @param reactEnabled + * whether React is enabled + * @param excludeWebComponentNpmPackages + * whether to exclude web component npm packages + */ + public ExclusionFilter(ClassFinder finder, boolean reactEnabled, + boolean excludeWebComponentNpmPackages) { this.finder = finder; this.reactEnabled = reactEnabled; + this.excludeWebComponentNpmPackages = excludeWebComponentNpmPackages; } /** @@ -95,7 +113,7 @@ private Set getExclusions(URL versionsResource) throws IOException { VersionsJsonConverter convert = new VersionsJsonConverter( Json.parse( IOUtils.toString(content, StandardCharsets.UTF_8)), - reactEnabled); + reactEnabled, excludeWebComponentNpmPackages); return convert.getExclusions(); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java index 1f2535feaca..21f6f0fa1f6 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/NodeUpdater.java @@ -177,7 +177,8 @@ private JsonObject getFilteredVersionsFromResource(URL versionsResource, Json.parse( IOUtils.toString(content, StandardCharsets.UTF_8)), options.isReactEnabled() - && FrontendUtils.isReactModuleAvailable(options)); + && FrontendUtils.isReactModuleAvailable(options), + options.isNpmExcludeWebComponents()); versionsJson = convert.getConvertedJson(); versionsJson = new VersionsJsonFilter(getPackageJson(), DEPENDENCIES) @@ -617,6 +618,10 @@ private void putHillaComponentsDependencies( if (options.isReactEnabled()) { dependencies.putAll(readDependenciesIfAvailable( "hilla/components/react", packageJsonKey)); + if (options.isNpmExcludeWebComponents()) { + // remove dependencies that depends on web components + dependencies.remove("@vaadin/hilla-react-crud"); + } } else { dependencies.putAll(readDependenciesIfAvailable( "hilla/components/lit", packageJsonKey)); diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java index db72b6a31bb..124bb597954 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/Options.java @@ -125,6 +125,8 @@ public class Options implements Serializable { private boolean reactEnable = true; + private boolean npmExcludeWebComponents = false; + /** * Removes generated files from a previous execution that are no more * created. @@ -967,4 +969,25 @@ public Options withCleanOldGeneratedFiles(boolean clean) { public boolean isCleanOldGeneratedFiles() { return cleanOldGeneratedFiles; } + + /** + * Sets whether to exclude web component npm packages in packages.json. + * + * @return this builder + */ + public boolean isNpmExcludeWebComponents() { + return npmExcludeWebComponents; + } + + /** + * Sets whether to exclude web component npm packages in packages.json. + * + * @param exclude + * whether to exclude web component npm packages + * @return this builder + */ + public Options withNpmExcludeWebComponents(boolean exclude) { + this.npmExcludeWebComponents = exclude; + return this; + } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java index 6017d5ceb55..94d157159a2 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskUpdatePackages.java @@ -217,7 +217,8 @@ private boolean updatePackageJsonDependencies(JsonObject packageJson, Map filteredApplicationDependencies = new ExclusionFilter( finder, options.isReactEnabled() - && FrontendUtils.isReactModuleAvailable(options)) + && FrontendUtils.isReactModuleAvailable(options), + options.isNpmExcludeWebComponents()) .exclude(applicationDependencies); // Add application dependencies diff --git a/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java b/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java index bfeb9afacd0..bd2219d8928 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/frontend/VersionsJsonConverter.java @@ -42,6 +42,7 @@ class VersionsJsonConverter { static final String VAADIN_CORE_NPM_PACKAGE = "@vaadin/vaadin-core"; + static final String VAADIN_BUNDLES = "@vaadin/bundles"; private static final String JS_VERSION = "jsVersion"; private static final String NPM_NAME = "npmName"; private static final String NPM_VERSION = "npmVersion"; @@ -74,15 +75,18 @@ class VersionsJsonConverter { private boolean reactEnabled; + private boolean excludeWebComponents; + private Set exclusions; private static Logger getLogger() { return LoggerFactory.getLogger(VersionsJsonConverter.class); } - VersionsJsonConverter(JsonObject platformVersions, - boolean collectReactComponents) { - this.reactEnabled = collectReactComponents; + VersionsJsonConverter(JsonObject platformVersions, boolean reactEnabled, + boolean excludeWebComponents) { + this.reactEnabled = reactEnabled; + this.excludeWebComponents = excludeWebComponents; exclusions = new HashSet<>(); convertedObject = Json.createObject(); @@ -135,6 +139,8 @@ private void excludeDependencies() { private boolean isIncludedByMode(String mode) { if (mode == null || mode.isBlank() || MODE_ALL.equalsIgnoreCase(mode)) { return true; + } else if (excludeWebComponents) { + return false; } else if (reactEnabled) { return MODE_REACT.equalsIgnoreCase(mode); } else { @@ -151,7 +157,19 @@ private void addDependency(JsonObject obj) { if (Objects.equals(npmName, VAADIN_CORE_NPM_PACKAGE)) { return; } + if (excludeWebComponents && Objects.equals(npmName, VAADIN_BUNDLES)) { + exclusions.add(npmName); + return; + } if (!isIncludedByMode(mode)) { + if (excludeWebComponents) { + // collecting exclusions also from non-included dependencies + // with a mode (react), when web components are not wanted. + if (MODE_REACT.equalsIgnoreCase(mode)) { + exclusions.add(npmName); + } + collectExclusions(obj); + } return; } if (obj.hasKey(NPM_VERSION)) { @@ -166,6 +184,12 @@ private void addDependency(JsonObject obj) { } convertedObject.put(npmName, version); + collectExclusions(obj); + getLogger().debug("versions.json adds dependency {} with version {}{}", + npmName, version, (mode != null ? " for mode " + mode : "")); + } + + private void collectExclusions(JsonObject obj) { if (obj.hasKey(EXCLUSIONS)) { JsonArray array = obj.getArray(EXCLUSIONS); if (array != null) { @@ -173,8 +197,6 @@ private void addDependency(JsonObject obj) { .forEach(i -> exclusions.add(array.getString(i))); } } - getLogger().debug("versions.json adds dependency {} with version {}{}", - npmName, version, (mode != null ? " for mode " + mode : "")); } } diff --git a/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java b/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java index 5063bbbc1d9..5c7b44a2381 100644 --- a/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java +++ b/flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java @@ -52,6 +52,7 @@ import static com.vaadin.flow.server.InitParameters.FRONTEND_HOTDEPLOY; import static com.vaadin.flow.server.InitParameters.NODE_DOWNLOAD_ROOT; import static com.vaadin.flow.server.InitParameters.NODE_VERSION; +import static com.vaadin.flow.server.InitParameters.NPM_EXCLUDE_WEB_COMPONENTS; import static com.vaadin.flow.server.InitParameters.REACT_ENABLE; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_ENABLE_DEV_SERVER; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_INITIAL_UIDL; @@ -187,6 +188,11 @@ protected Map getConfigParametersUsingTokenData( String.valueOf(buildInfo.getBoolean(PREMIUM_FEATURES))); } + if (buildInfo.hasKey(NPM_EXCLUDE_WEB_COMPONENTS)) { + params.put(NPM_EXCLUDE_WEB_COMPONENTS, String + .valueOf(buildInfo.getBoolean(NPM_EXCLUDE_WEB_COMPONENTS))); + } + setDevModePropertiesUsingTokenData(params, buildInfo); return params; } diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java index 0cef96601bf..8bfa41a1c36 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/NodeUpdaterTest.java @@ -470,6 +470,49 @@ public void testGetPlatformPinnedDependencies_reactNotAvailable_noReactComponent @Test public void testGetPlatformPinnedDependencies_reactAvailable_containsReactComponents() throws IOException, ClassNotFoundException { + generateTestDataForReactComponents(); + + JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); + + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/react-components")); + Assert.assertTrue( + pinnedVersions.hasKey("@vaadin/react-components-pro")); + } + + @Test + public void testGetPlatformPinnedDependencies_reactAvailable_excludeWebComponents() + throws IOException, ClassNotFoundException { + options.withNpmExcludeWebComponents(true); + generateTestDataForReactComponents(); + + JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); + + // @vaadin/button doesn't have 'mode' set, so it should be included + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); + Assert.assertFalse(pinnedVersions.hasKey("@vaadin/react-components")); + Assert.assertFalse( + pinnedVersions.hasKey("@vaadin/react-components-pro")); + } + + @Test + public void testGetPlatformPinnedDependencies_reactDisabled_excludeWebComponents() + throws IOException, ClassNotFoundException { + options.withReact(false); + options.withNpmExcludeWebComponents(true); + generateTestDataForReactComponents(); + + JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); + + // @vaadin/button doesn't have 'mode' set, so it should be included + Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); + Assert.assertFalse(pinnedVersions.hasKey("@vaadin/react-components")); + Assert.assertFalse( + pinnedVersions.hasKey("@vaadin/react-components-pro")); + } + + private void generateTestDataForReactComponents() + throws IOException, ClassNotFoundException { File coreVersionsFile = File.createTempFile("vaadin-core-versions", ".json", temporaryFolder.newFolder()); File vaadinVersionsFile = File.createTempFile("vaadin-versions", @@ -514,13 +557,6 @@ public void testGetPlatformPinnedDependencies_reactAvailable_containsReactCompon Class clazz = FeatureFlags.class; // actual class doesn't matter Mockito.doReturn(clazz).when(finder).loadClass( "com.vaadin.flow.component.react.ReactAdapterComponent"); - - JsonObject pinnedVersions = nodeUpdater.getPlatformPinnedDependencies(); - - Assert.assertTrue(pinnedVersions.hasKey("@vaadin/button")); - Assert.assertTrue(pinnedVersions.hasKey("@vaadin/react-components")); - Assert.assertTrue( - pinnedVersions.hasKey("@vaadin/react-components-pro")); } @Test diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java index 196cf990dca..298aabb27e2 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskUpdatePackagesNpmTest.java @@ -23,6 +23,8 @@ import static com.vaadin.flow.server.frontend.NodeUpdater.OVERRIDES; import static com.vaadin.flow.server.frontend.NodeUpdater.VAADIN_DEP_KEY; import static com.vaadin.flow.server.frontend.VersionsJsonConverter.VAADIN_CORE_NPM_PACKAGE; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; import java.io.File; import java.io.IOException; @@ -50,7 +52,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.vaadin.flow.di.Lookup; import com.vaadin.flow.server.Constants; import com.vaadin.flow.server.frontend.scanner.ClassFinder; import com.vaadin.flow.server.frontend.scanner.FrontendDependencies; @@ -848,6 +849,163 @@ public void reactDisabled_coreDependenciesAdded() throws IOException { } + @Test + public void webComponentsExcluded_reactDisabled_noExclusionsInVersions() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(false) + .withNpmExcludeWebComponents(true); + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertTrue(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + @Test + public void webComponentsExcluded_reactDisabled_exclusionsInVersions_noWebComponentsIncluded() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION, + Set.of(VAADIN_DIALOG)); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(false) + .withNpmExcludeWebComponents(true); + + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + @Test + public void webComponentsExcluded_reactEnabled_noExclusionsInVersions() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(true) + .withNpmExcludeWebComponents(true); + + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertTrue(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + @Test + public void webComponentsExcluded_reactEnabled_exclusionsInVersions_noWebComponentsIncluded() + throws IOException { + createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, + PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION, + Set.of(VAADIN_DIALOG)); + Options options = new MockOptions(finder, npmFolder) + .withBuildDirectory(TARGET).withEnablePnpm(false) + .withBundleBuild(true).withReact(true) + .withNpmExcludeWebComponents(true); + + // with scanned application dependencies + execTaskUpdatePackages(createApplicationDependencies(), options); + JsonObject pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + + // without scanned application dependencies + execTaskUpdatePackages(new HashMap<>(), options); + pkgJson = getOrCreatePackageJson(); + + assertFalse(hasInDependencies(pkgJson, VAADIN_DIALOG)); + assertFalse(hasInVaadinDependencies(pkgJson, VAADIN_DIALOG)); + assertTrue(hasInDependencies(pkgJson, VAADIN_OVERLAY)); + assertTrue(hasInVaadinDependencies(pkgJson, VAADIN_OVERLAY)); + assertFalse(hasInDependencies(pkgJson, REACT_COMPONENTS)); + assertFalse(hasInVaadinDependencies(pkgJson, REACT_COMPONENTS)); + } + + private void execTaskUpdatePackages( + Map scannedApplicationDependencies, + Options options) { + final FrontendDependencies frontendDependenciesScanner = Mockito + .mock(FrontendDependencies.class); + Mockito.when(frontendDependenciesScanner.getPackages()) + .thenReturn(scannedApplicationDependencies); + final TaskUpdatePackages task = new TaskUpdatePackages( + frontendDependenciesScanner, options) { + }; + task.execute(); + } + + private boolean hasInDependencies(JsonObject newPackageJson, String key) { + return newPackageJson.hasKey("dependencies") + && newPackageJson.getObject("dependencies").hasKey(key); + } + + private boolean hasInVaadinDependencies(JsonObject newPackageJson, + String key) { + return newPackageJson.hasKey("vaadin") && newPackageJson + .getObject("vaadin").getObject("dependencies").hasKey(key); + } + private void createBasicVaadinVersionsJson() { createVaadinVersionsJson(PLATFORM_DIALOG_VERSION, PLATFORM_ELEMENT_MIXIN_VERSION, PLATFORM_OVERLAY_VERSION); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java b/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java index 4a9a3aecfc6..f5b694c66fb 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/frontend/VersionsJsonConverterTest.java @@ -64,7 +64,7 @@ public void convertPlatformVersions() throws IOException { // @formatter:on VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), false); + Json.parse(json), false, false); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -127,7 +127,7 @@ public void reactRouterInUse_reactComponentsAreAdded() { """.formatted(VAADIN_CORE_NPM_PACKAGE); VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), true); + Json.parse(json), true, true); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -202,7 +202,7 @@ public void reactRouterNotUsed_reactComponentsIgnored() { """.formatted(VAADIN_CORE_NPM_PACKAGE); VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), false); + Json.parse(json), false, true); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -278,7 +278,7 @@ public void testModeProperty() { // react enabled VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), true); + Json.parse(json), true, false); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -295,8 +295,27 @@ public void testModeProperty() { Assert.assertFalse(convertedJson.hasKey("react-components")); Assert.assertFalse(convertedJson.hasKey("react-components-pro")); + // react enabled, exclude web components + convert = new VersionsJsonConverter(Json.parse(json), true, true); + convertedJson = convert.getConvertedJson(); + Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); + Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); + Assert.assertTrue(convertedJson.hasKey("@polymer/iron-list")); + Assert.assertFalse( + convertedJson.hasKey("@vaadin/react-components-pro")); + Assert.assertFalse(convertedJson.hasKey("@vaadin/react-components")); + + Assert.assertFalse(convertedJson.hasKey("flow")); + Assert.assertFalse(convertedJson.hasKey("core")); + Assert.assertFalse(convertedJson.hasKey(VAADIN_CORE_NPM_PACKAGE)); + Assert.assertFalse(convertedJson.hasKey("platform")); + Assert.assertFalse(convertedJson.hasKey("react")); + Assert.assertFalse(convertedJson.hasKey("react-pro")); + Assert.assertFalse(convertedJson.hasKey("react-components")); + Assert.assertFalse(convertedJson.hasKey("react-components-pro")); + // react disabled - convert = new VersionsJsonConverter(Json.parse(json), false); + convert = new VersionsJsonConverter(Json.parse(json), false, false); convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -305,6 +324,24 @@ public void testModeProperty() { convertedJson.hasKey("@vaadin/react-components-pro")); Assert.assertFalse(convertedJson.hasKey("@vaadin/react-components")); + Assert.assertFalse(convertedJson.hasKey("flow")); + Assert.assertFalse(convertedJson.hasKey("core")); + Assert.assertFalse(convertedJson.hasKey(VAADIN_CORE_NPM_PACKAGE)); + Assert.assertFalse(convertedJson.hasKey("platform")); + Assert.assertFalse(convertedJson.hasKey("react")); + Assert.assertFalse(convertedJson.hasKey("react-pro")); + Assert.assertFalse(convertedJson.hasKey("react-components")); + + // react disabled, exclude web components + convert = new VersionsJsonConverter(Json.parse(json), false, true); + convertedJson = convert.getConvertedJson(); + Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); + Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); + Assert.assertTrue(convertedJson.hasKey("@polymer/iron-list")); + Assert.assertFalse( + convertedJson.hasKey("@vaadin/react-components-pro")); + Assert.assertFalse(convertedJson.hasKey("@vaadin/react-components")); + Assert.assertFalse(convertedJson.hasKey("flow")); Assert.assertFalse(convertedJson.hasKey("core")); Assert.assertFalse(convertedJson.hasKey(VAADIN_CORE_NPM_PACKAGE)); @@ -371,7 +408,7 @@ public void testExclusionsArrayProperty() { // react enabled VersionsJsonConverter convert = new VersionsJsonConverter( - Json.parse(json), true); + Json.parse(json), true, false); JsonObject convertedJson = convert.getConvertedJson(); Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertFalse(convertedJson.hasKey("@vaadin/vaadin-upload")); @@ -390,7 +427,7 @@ public void testExclusionsArrayProperty() { Assert.assertFalse(convertedJson.hasKey("react-components-pro")); // react disabled - convert = new VersionsJsonConverter(Json.parse(json), false); + convert = new VersionsJsonConverter(Json.parse(json), false, false); convertedJson = convert.getConvertedJson(); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-progress-bar")); Assert.assertTrue(convertedJson.hasKey("@vaadin/vaadin-upload")); diff --git a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java index 67b2bc729b9..02665d9c1fc 100644 --- a/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java +++ b/vaadin-dev-server/src/main/java/com/vaadin/base/devserver/startup/DevModeInitializer.java @@ -81,6 +81,7 @@ import static com.vaadin.flow.server.Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN; import static com.vaadin.flow.server.Constants.VAADIN_SERVLET_RESOURCES; import static com.vaadin.flow.server.Constants.VAADIN_WEBAPP_RESOURCES; +import static com.vaadin.flow.server.InitParameters.NPM_EXCLUDE_WEB_COMPONENTS; import static com.vaadin.flow.server.InitParameters.REACT_ENABLE; import static com.vaadin.flow.server.InitParameters.SERVLET_PARAMETER_DEVMODE_OPTIMIZE_BUNDLE; import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED; @@ -270,6 +271,8 @@ public static DevModeHandler initDevModeHandler(Set> classes, boolean reactEnable = config.getBooleanProperty(REACT_ENABLE, FrontendUtils .isReactRouterRequired(options.getFrontendDirectory())); + boolean npmExcludeWebComponents = config + .getBooleanProperty(NPM_EXCLUDE_WEB_COMPONENTS, false); options.enablePackagesUpdate(true) .useByteCodeScanner(useByteCodeScanner) .withFrontendGeneratedFolder(frontendGeneratedFolder) @@ -289,7 +292,8 @@ public static DevModeHandler initDevModeHandler(Set> classes, .withFrontendHotdeploy( mode == Mode.DEVELOPMENT_FRONTEND_LIVERELOAD) .withBundleBuild(mode == Mode.DEVELOPMENT_BUNDLE) - .withReact(reactEnable); + .withReact(reactEnable) + .withNpmExcludeWebComponents(npmExcludeWebComponents); NodeTasks tasks = new NodeTasks(options); From 4a08a103335015b1495d13efb76293d33be724d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 16:00:34 +0200 Subject: [PATCH 6/6] chore(deps): bump jetty.version from 12.0.14 to 12.0.15 (#20432) Bumps `jetty.version` from 12.0.14 to 12.0.15. Updates `org.eclipse.jetty.ee10:jetty-ee10-maven-plugin` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10:jetty-ee10-servlets` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty:jetty-server` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10:jetty-ee10-webapp` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty:jetty-http` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10:jetty-ee10-annotations` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty:jetty-util` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10:jetty-ee10-servlet` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10:jetty-ee10-plus` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10:jetty-ee10-proxy` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty:jetty-client` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty:jetty-io` from 12.0.14 to 12.0.15 Updates `org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client` from 12.0.14 to 12.0.15 --- updated-dependencies: - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-servlets dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-server dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-webapp dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-http dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-annotations dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-util dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-servlet dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-plus dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10:jetty-ee10-proxy dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-client dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty:jetty-io dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tomi Virtanen --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 142097c86b2..13ad6b6ef8f 100644 --- a/pom.xml +++ b/pom.xml @@ -107,7 +107,7 @@ 3.4.0 1.6.0 9.3.5 - 12.0.14 + 12.0.15 1.2.1 3.6.0 3.9.9