Skip to content

Commit e499842

Browse files
committed
fix: compute client-side feature flag values at runtime
Activating a feature flag in the project does not work when a default bundle is used because the values of the flags sent to the client are hard-coded at bundle build time. Additionally, if a feature flag is active during bundle creation, it remains active on the client side even if the project does not activate it in the `vaadin-featureflags.properties` file. This change ensures that feature flags in the frontend bundle are always disabled initially and are activated on page load based on the current project settings. Fixes #20991
1 parent 6f5db3b commit e499842

File tree

9 files changed

+103
-22
lines changed

9 files changed

+103
-22
lines changed

flow-plugins/flow-plugin-base/src/test/java/com/vaadin/flow/plugin/base/BuildFrontendUtilTest.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,9 @@ public void runNodeUpdater_generateFeatureFlagsJsFile() throws Exception {
531531
.readString(generatedFeatureFlagsFile.toPath())
532532
.replace("\r\n", "\n");
533533

534-
Assert.assertTrue("Example feature flag is not set",
534+
Assert.assertTrue("Example feature should not be set at build time",
535535
featureFlagsJs.contains(
536-
"window.Vaadin.featureFlags.exampleFeatureFlag = true;\n"));
536+
"window.Vaadin.featureFlags.exampleFeatureFlag = false;\n"));
537537
}
538538

539539
private void fillAdapter() throws URISyntaxException {

flow-server/src/main/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandler.java

+27
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.List;
2626
import java.util.Locale;
2727
import java.util.Optional;
28+
import java.util.stream.Collectors;
2829
import java.util.stream.Stream;
2930

3031
import org.apache.commons.io.FilenameUtils;
@@ -37,6 +38,8 @@
3738
import org.slf4j.Logger;
3839
import org.slf4j.LoggerFactory;
3940

41+
import com.vaadin.experimental.Feature;
42+
import com.vaadin.experimental.FeatureFlags;
4043
import com.vaadin.flow.component.UI;
4144
import com.vaadin.flow.function.DeploymentConfiguration;
4245
import com.vaadin.flow.internal.BootstrapHandlerHelper;
@@ -110,6 +113,8 @@ public boolean synchronizedHandleRequest(VaadinSession session,
110113
htmlElement.attr("lang", locale.getLanguage());
111114
}
112115

116+
initializeFeatureFlags(indexDocument, request);
117+
113118
JsonObject initialJson = Json.createObject();
114119

115120
if (service.getBootstrapInitialPredicate()
@@ -209,6 +214,28 @@ public boolean synchronizedHandleRequest(VaadinSession session,
209214
return true;
210215
}
211216

217+
private void initializeFeatureFlags(Document indexDocument,
218+
VaadinRequest request) {
219+
String script = featureFlagsInitializer(request);
220+
Element scriptElement = indexDocument.head().prependElement("script");
221+
scriptElement.attr(SCRIPT_INITIAL, "");
222+
scriptElement.appendChild(new DataNode(script));
223+
}
224+
225+
static String featureFlagsInitializer(VaadinRequest request) {
226+
return FeatureFlags.get(request.getService().getContext()).getFeatures()
227+
.stream().filter(Feature::isEnabled)
228+
.map(feature -> String.format("activator(\"%s\");",
229+
feature.getId()))
230+
.collect(Collectors.joining("\n",
231+
"""
232+
window.Vaadin = window.Vaadin || {};
233+
window.Vaadin.featureFlagsUpdaters = window.Vaadin.featureFlagsUpdaters || [];
234+
window.Vaadin.featureFlagsUpdaters.push((activator) => {
235+
""",
236+
"});"));
237+
}
238+
212239
private static void addDevBundleTheme(Document document,
213240
VaadinContext context) {
214241
ApplicationConfiguration config = ApplicationConfiguration.get(context);

flow-server/src/main/java/com/vaadin/flow/server/communication/WebComponentProvider.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ protected String generateNPMResponse(String tagName, VaadinRequest request,
193193
// get the running script
194194
boolean productionMode = request.getService()
195195
.getDeploymentConfiguration().isProductionMode();
196-
return getThisScript(tagName) + "var scriptUri = thisScript.src;"
196+
197+
return IndexHtmlRequestHandler.featureFlagsInitializer(request)
198+
+ getThisScript(tagName) + "var scriptUri = thisScript.src;"
197199
+ "var index = scriptUri.lastIndexOf('" + WEB_COMPONENT_PATH
198200
+ "');" + "var context = scriptUri.substring(0, index+"
199201
+ WEB_COMPONENT_PATH.length() + ");"

flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateFeatureFlags.java

+29-8
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
*/
1616
package com.vaadin.flow.server.frontend;
1717

18-
import com.vaadin.experimental.FeatureFlags;
19-
2018
import java.io.File;
2119
import java.util.ArrayList;
2220
import java.util.List;
2321

24-
import static com.vaadin.flow.server.frontend.FrontendUtils.*;
22+
import com.vaadin.experimental.Feature;
23+
24+
import static com.vaadin.flow.server.frontend.FrontendUtils.FEATURE_FLAGS_FILE_NAME;
25+
import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED;
2526

2627
/**
2728
* A task for generating the feature flags file
@@ -47,11 +48,31 @@ protected String getFileContent() {
4748
lines.add(
4849
"window.Vaadin.featureFlags = window.Vaadin.featureFlags || {};");
4950

50-
FeatureFlags featureFlags = options.getFeatureFlags();
51-
featureFlags.getFeatures().forEach(feature -> {
52-
lines.add(String.format("window.Vaadin.featureFlags.%s = %s;",
53-
feature.getId(), featureFlags.isEnabled(feature)));
54-
});
51+
// Initialize the flag entries only once. For exported web-components,
52+
// this script may be executed multiple times (one per embedded
53+
// component) and we should prevent active flags get overridden.
54+
List<Feature> featureFlags = options.getFeatureFlags().getFeatures();
55+
if (!featureFlags.isEmpty()) {
56+
lines.add(
57+
"if (Object.keys(window.Vaadin.featureFlags).length === 0) {");
58+
featureFlags.forEach(feature -> {
59+
lines.add(
60+
String.format("window.Vaadin.featureFlags.%s = false;",
61+
feature.getId()));
62+
});
63+
lines.add("};");
64+
}
65+
66+
// Multiple feature flags updater functions can be registered, in case
67+
// of exported web-component. If the component comes from different web
68+
// applications, the active flags might not be the same.
69+
lines.add("if (window.Vaadin.featureFlagsUpdaters) { ");
70+
lines.add(
71+
"const activator = (id) => window.Vaadin.featureFlags[id] = true;");
72+
lines.add(
73+
"window.Vaadin.featureFlagsUpdaters.forEach(updater => updater(activator));");
74+
lines.add("delete window.Vaadin.featureFlagsUpdaters;");
75+
lines.add("} ");
5576

5677
// See https://github.com/vaadin/flow/issues/14184
5778
lines.add("export {};");

flow-server/src/main/java/com/vaadin/flow/server/frontend/TaskGenerateWebComponentBootstrap.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import static com.vaadin.flow.server.frontend.FrontendUtils.FEATURE_FLAGS_FILE_NAME;
2223
import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED;
2324
import static com.vaadin.flow.server.frontend.FrontendUtils.WEB_COMPONENT_BOOTSTRAP_FILE_NAME;
2425

@@ -51,7 +52,7 @@ public class TaskGenerateWebComponentBootstrap
5152
@Override
5253
protected String getFileContent() {
5354
List<String> lines = new ArrayList<>();
54-
55+
lines.add(String.format("import './%s';%n", FEATURE_FLAGS_FILE_NAME));
5556
lines.add("import 'Frontend/generated/flow/"
5657
+ FrontendUtils.IMPORTS_WEB_COMPONENT_NAME + "';");
5758
lines.add("import { init } from '" + FrontendUtils.JAR_RESOURCES_IMPORT

flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java

+11
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,17 @@ public void serveIndexHtml_requestWithSomePath_hasBaseHrefElement()
210210
indexHtml.contains("<base href=\"./..\""));
211211
}
212212

213+
@Test
214+
public void serveIndexHtml_featureFlagsSetter_isPresent()
215+
throws IOException {
216+
indexHtmlRequestHandler.synchronizedHandleRequest(session,
217+
createVaadinRequest("/"), response);
218+
String indexHtml = responseOutput.toString(StandardCharsets.UTF_8);
219+
Assert.assertTrue("Response should have Feature Flags updater function",
220+
indexHtml.contains(
221+
"window.Vaadin.featureFlagsUpdaters.push((activator) => {"));
222+
}
223+
213224
@Test
214225
public void canHandleRequest_requestWithRootPath_handleRequest() {
215226
boolean canHandleRequest = indexHtmlRequestHandler

flow-server/src/test/java/com/vaadin/flow/server/communication/WebComponentProviderTest.java

+16-2
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
package com.vaadin.flow.server.communication;
1818

1919
import jakarta.servlet.ServletContext;
20-
2120
import java.io.ByteArrayOutputStream;
2221
import java.io.IOException;
2322
import java.util.HashSet;
2423
import java.util.Set;
24+
import java.util.function.Supplier;
2525
import java.util.stream.Collectors;
2626
import java.util.stream.Stream;
2727

@@ -31,6 +31,7 @@
3131
import org.junit.Before;
3232
import org.junit.Test;
3333
import org.mockito.ArgumentCaptor;
34+
import org.mockito.ArgumentMatchers;
3435
import org.mockito.Mock;
3536
import org.mockito.Mockito;
3637
import org.mockito.MockitoAnnotations;
@@ -42,6 +43,7 @@
4243
import com.vaadin.flow.component.page.Push;
4344
import com.vaadin.flow.component.webcomponent.WebComponent;
4445
import com.vaadin.flow.component.webcomponent.WebComponentConfiguration;
46+
import com.vaadin.flow.di.Lookup;
4547
import com.vaadin.flow.function.DeploymentConfiguration;
4648
import com.vaadin.flow.internal.CurrentInstance;
4749
import com.vaadin.flow.server.DefaultDeploymentConfiguration;
@@ -101,6 +103,14 @@ public void init() {
101103
.getArguments()[0])
102104
.when(context)
103105
.setAttribute(any(WebComponentConfigurationRegistry.class));
106+
107+
final Lookup lookup = Mockito.mock(Lookup.class);
108+
Mockito.when(context.getAttribute(Lookup.class)).thenReturn(lookup);
109+
Mockito.doAnswer(i -> i.getArgument(1, Supplier.class).get())
110+
.when(context).getAttribute(
111+
ArgumentMatchers.argThat(aClass -> "FeatureFlagsWrapper"
112+
.equals(aClass.getSimpleName())),
113+
any());
104114
VaadinService.setCurrent(service);
105115
Mockito.when(service.getInstantiator())
106116
.thenReturn(new MockInstantiator());
@@ -178,7 +188,7 @@ public void webComponentNotPresent_responseReturns404() throws IOException {
178188
public void webComponentGenerator_responseGetsResult() throws IOException {
179189
registry = setupConfigurations(MyComponentExporter.class);
180190

181-
ByteArrayOutputStream out = Mockito.mock(ByteArrayOutputStream.class);
191+
ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream());
182192

183193
DefaultDeploymentConfiguration configuration = Mockito
184194
.mock(DefaultDeploymentConfiguration.class);
@@ -191,6 +201,10 @@ public void webComponentGenerator_responseGetsResult() throws IOException {
191201
Assert.assertTrue("Provider should handle web-component request",
192202
provider.synchronizedHandleRequest(session, request, response));
193203

204+
Assert.assertTrue("Response should have Feature Flags updater function",
205+
out.toString().contains(
206+
"window.Vaadin.featureFlagsUpdaters.push((activator) => {"));
207+
194208
Mockito.verify(response).getOutputStream();
195209
Mockito.verify(out).write(Mockito.any(), Mockito.anyInt(),
196210
Mockito.anyInt());

flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateFeatureFlagsTest.java

+3-8
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,12 @@ public void should_defineAllFeatureFlags() throws ExecutionFailedException {
8787
}
8888

8989
@Test
90-
public void should_defineCorrectEnabledValue()
90+
public void should_callFeatureFlagsUpdaterFunction()
9191
throws ExecutionFailedException {
92-
// Enable example feature
93-
featureFlags.getFeatures().stream()
94-
.filter(feature -> feature.equals(FeatureFlags.EXAMPLE))
95-
.forEach(feature -> feature.setEnabled(true));
96-
9792
taskGenerateFeatureFlags.execute();
9893
String content = taskGenerateFeatureFlags.getFileContent();
99-
100-
assertFeatureFlagGlobal(content, FeatureFlags.EXAMPLE, true);
94+
Assert.assertTrue(content.contains(
95+
"window.Vaadin.featureFlagsUpdaters.forEach(updater => updater(activator))"));
10196
}
10297

10398
@Test

flow-server/src/test/java/com/vaadin/flow/server/frontend/TaskGenerateWebComponentBootstrapTest.java

+10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.mockito.Mockito;
2828

2929
import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
30+
import static com.vaadin.flow.server.frontend.FrontendUtils.FEATURE_FLAGS_FILE_NAME;
3031

3132
public class TaskGenerateWebComponentBootstrapTest {
3233
@Rule
@@ -68,4 +69,13 @@ public void should_importAndInitializeFlowClient()
6869
"import { init } from '" + FrontendUtils.JAR_RESOURCES_IMPORT
6970
+ "FlowClient.js';\n" + "init()"));
7071
}
72+
73+
@Test
74+
public void should_importFeatureFlagTS() throws ExecutionFailedException {
75+
taskGenerateWebComponentBootstrap.execute();
76+
String content = taskGenerateWebComponentBootstrap.getFileContent();
77+
Assert.assertTrue(content.contains(
78+
String.format("import './%s';", FEATURE_FLAGS_FILE_NAME)));
79+
}
80+
7181
}

0 commit comments

Comments
 (0)