Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: compute client-side feature flag values at runtime #21066

Merged
merged 1 commit into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -532,9 +532,9 @@ public void runNodeUpdater_generateFeatureFlagsJsFile() throws Exception {
.readString(generatedFeatureFlagsFile.toPath())
.replace("\r\n", "\n");

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

private void fillAdapter() throws URISyntaxException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.JsonNode;
Expand All @@ -40,6 +41,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.vaadin.experimental.Feature;
import com.vaadin.experimental.FeatureFlags;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.internal.BootstrapHandlerHelper;
Expand Down Expand Up @@ -108,6 +111,8 @@ public boolean synchronizedHandleRequest(VaadinSession session,
htmlElement.attr("lang", locale.getLanguage());
}

initializeFeatureFlags(indexDocument, request);

ObjectNode initialJson = JacksonUtils.createObjectNode();

if (service.getBootstrapInitialPredicate()
Expand Down Expand Up @@ -207,6 +212,28 @@ public boolean synchronizedHandleRequest(VaadinSession session,
return true;
}

private void initializeFeatureFlags(Document indexDocument,
VaadinRequest request) {
String script = featureFlagsInitializer(request);
Element scriptElement = indexDocument.head().prependElement("script");
scriptElement.attr(SCRIPT_INITIAL, "");
scriptElement.appendChild(new DataNode(script));
}

static String featureFlagsInitializer(VaadinRequest request) {
return FeatureFlags.get(request.getService().getContext()).getFeatures()
.stream().filter(Feature::isEnabled)
.map(feature -> String.format("activator(\"%s\");",
feature.getId()))
.collect(Collectors.joining("\n",
"""
window.Vaadin = window.Vaadin || {};
window.Vaadin.featureFlagsUpdaters = window.Vaadin.featureFlagsUpdaters || [];
window.Vaadin.featureFlagsUpdaters.push((activator) => {
""",
"});"));
}

private static void addDevBundleTheme(Document document,
VaadinContext context) {
ApplicationConfiguration config = ApplicationConfiguration.get(context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ protected String generateNPMResponse(String tagName, VaadinRequest request,
// get the running script
boolean productionMode = request.getService()
.getDeploymentConfiguration().isProductionMode();
return getThisScript(tagName) + "var scriptUri = thisScript.src;"

return IndexHtmlRequestHandler.featureFlagsInitializer(request)
+ getThisScript(tagName) + "var scriptUri = thisScript.src;"
+ "var index = scriptUri.lastIndexOf('" + WEB_COMPONENT_PATH
+ "');" + "var context = scriptUri.substring(0, index+"
+ WEB_COMPONENT_PATH.length() + ");"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
*/
package com.vaadin.flow.server.frontend;

import com.vaadin.experimental.FeatureFlags;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import static com.vaadin.flow.server.frontend.FrontendUtils.*;
import com.vaadin.experimental.Feature;

import static com.vaadin.flow.server.frontend.FrontendUtils.FEATURE_FLAGS_FILE_NAME;
import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED;

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

FeatureFlags featureFlags = options.getFeatureFlags();
featureFlags.getFeatures().forEach(feature -> {
lines.add(String.format("window.Vaadin.featureFlags.%s = %s;",
feature.getId(), featureFlags.isEnabled(feature)));
});
// Initialize the flag entries only once. For exported web-components,
// this script may be executed multiple times (one per embedded
// component) and we should prevent active flags get overridden.
List<Feature> featureFlags = options.getFeatureFlags().getFeatures();
if (!featureFlags.isEmpty()) {
lines.add(
"if (Object.keys(window.Vaadin.featureFlags).length === 0) {");
featureFlags.forEach(feature -> {
lines.add(
String.format("window.Vaadin.featureFlags.%s = false;",
feature.getId()));
});
lines.add("};");
}

// Multiple feature flags updater functions can be registered, in case
// of exported web-component. If the component comes from different web
// applications, the active flags might not be the same.
lines.add("if (window.Vaadin.featureFlagsUpdaters) { ");
lines.add(
"const activator = (id) => window.Vaadin.featureFlags[id] = true;");
lines.add(
"window.Vaadin.featureFlagsUpdaters.forEach(updater => updater(activator));");
lines.add("delete window.Vaadin.featureFlagsUpdaters;");
lines.add("} ");

// See https://github.com/vaadin/flow/issues/14184
lines.add("export {};");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.ArrayList;
import java.util.List;

import static com.vaadin.flow.server.frontend.FrontendUtils.FEATURE_FLAGS_FILE_NAME;
import static com.vaadin.flow.server.frontend.FrontendUtils.GENERATED;
import static com.vaadin.flow.server.frontend.FrontendUtils.WEB_COMPONENT_BOOTSTRAP_FILE_NAME;

Expand Down Expand Up @@ -51,7 +52,7 @@ public class TaskGenerateWebComponentBootstrap
@Override
protected String getFileContent() {
List<String> lines = new ArrayList<>();

lines.add(String.format("import './%s';%n", FEATURE_FLAGS_FILE_NAME));
lines.add("import 'Frontend/generated/flow/"
+ FrontendUtils.IMPORTS_WEB_COMPONENT_NAME + "';");
lines.add("import { init } from '" + FrontendUtils.JAR_RESOURCES_IMPORT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ public void serveIndexHtml_requestWithSomePath_hasBaseHrefElement()
indexHtml.contains("<base href=\"./..\""));
}

@Test
public void serveIndexHtml_featureFlagsSetter_isPresent()
throws IOException {
indexHtmlRequestHandler.synchronizedHandleRequest(session,
createVaadinRequest("/"), response);
String indexHtml = responseOutput.toString(StandardCharsets.UTF_8);
Assert.assertTrue("Response should have Feature Flags updater function",
indexHtml.contains(
"window.Vaadin.featureFlagsUpdaters.push((activator) => {"));
}

@Test
public void canHandleRequest_requestWithRootPath_handleRequest() {
boolean canHandleRequest = indexHtmlRequestHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
package com.vaadin.flow.server.communication;

import jakarta.servlet.ServletContext;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -31,6 +31,7 @@
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
Expand All @@ -42,6 +43,7 @@
import com.vaadin.flow.component.page.Push;
import com.vaadin.flow.component.webcomponent.WebComponent;
import com.vaadin.flow.component.webcomponent.WebComponentConfiguration;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.function.DeploymentConfiguration;
import com.vaadin.flow.internal.CurrentInstance;
import com.vaadin.flow.server.DefaultDeploymentConfiguration;
Expand Down Expand Up @@ -101,6 +103,14 @@ public void init() {
.getArguments()[0])
.when(context)
.setAttribute(any(WebComponentConfigurationRegistry.class));

final Lookup lookup = Mockito.mock(Lookup.class);
Mockito.when(context.getAttribute(Lookup.class)).thenReturn(lookup);
Mockito.doAnswer(i -> i.getArgument(1, Supplier.class).get())
.when(context).getAttribute(
ArgumentMatchers.argThat(aClass -> "FeatureFlagsWrapper"
.equals(aClass.getSimpleName())),
any());
VaadinService.setCurrent(service);
Mockito.when(service.getInstantiator())
.thenReturn(new MockInstantiator());
Expand Down Expand Up @@ -178,7 +188,7 @@ public void webComponentNotPresent_responseReturns404() throws IOException {
public void webComponentGenerator_responseGetsResult() throws IOException {
registry = setupConfigurations(MyComponentExporter.class);

ByteArrayOutputStream out = Mockito.mock(ByteArrayOutputStream.class);
ByteArrayOutputStream out = Mockito.spy(new ByteArrayOutputStream());

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

Assert.assertTrue("Response should have Feature Flags updater function",
out.toString().contains(
"window.Vaadin.featureFlagsUpdaters.push((activator) => {"));

Mockito.verify(response).getOutputStream();
Mockito.verify(out).write(Mockito.any(), Mockito.anyInt(),
Mockito.anyInt());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,12 @@ public void should_defineAllFeatureFlags() throws ExecutionFailedException {
}

@Test
public void should_defineCorrectEnabledValue()
public void should_callFeatureFlagsUpdaterFunction()
throws ExecutionFailedException {
// Enable example feature
featureFlags.getFeatures().stream()
.filter(feature -> feature.equals(FeatureFlags.EXAMPLE))
.forEach(feature -> feature.setEnabled(true));

taskGenerateFeatureFlags.execute();
String content = taskGenerateFeatureFlags.getFileContent();

assertFeatureFlagGlobal(content, FeatureFlags.EXAMPLE, true);
Assert.assertTrue(content.contains(
"window.Vaadin.featureFlagsUpdaters.forEach(updater => updater(activator))"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.mockito.Mockito;

import static com.vaadin.flow.server.frontend.FrontendUtils.DEFAULT_FRONTEND_DIR;
import static com.vaadin.flow.server.frontend.FrontendUtils.FEATURE_FLAGS_FILE_NAME;

public class TaskGenerateWebComponentBootstrapTest {
@Rule
Expand Down Expand Up @@ -68,4 +69,13 @@ public void should_importAndInitializeFlowClient()
"import { init } from '" + FrontendUtils.JAR_RESOURCES_IMPORT
+ "FlowClient.js';\n" + "init()"));
}

@Test
public void should_importFeatureFlagTS() throws ExecutionFailedException {
taskGenerateWebComponentBootstrap.execute();
String content = taskGenerateWebComponentBootstrap.getFileContent();
Assert.assertTrue(content.contains(
String.format("import './%s';", FEATURE_FLAGS_FILE_NAME)));
}

}
Loading