Skip to content

Commit 3d3747f

Browse files
mcollovatimshabarov
authored andcommitted
feat: show watermark in production mode (#21915)
Shows a watermark component at runtime for watermarked applications running in production. The watermark component is added to the bundle at build time and activated if needed at runtime.
1 parent 2df0aa8 commit 3d3747f

File tree

23 files changed

+1087
-6
lines changed

23 files changed

+1087
-6
lines changed

flow-plugins/flow-plugin-base/src/main/java/com/vaadin/flow/plugin/base/BuildFrontendUtil.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,8 @@ public static void runNodeUpdater(PluginAdapterBuild adapter,
372372
adapter.frontendExtraFileExtensions())
373373
.withFrontendIgnoreVersionChecks(
374374
adapter.isFrontendIgnoreVersionChecks())
375-
.withFrontendDependenciesScanner(frontendDependencies);
375+
.withFrontendDependenciesScanner(frontendDependencies)
376+
.withWatermarkEnable(adapter.isWatermarkEnabled());
376377
new NodeTasks(options).execute();
377378
} catch (ExecutionFailedException exception) {
378379
throw exception;

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ public boolean synchronizedHandleRequest(VaadinSession session,
202202
// page is constructed
203203
service.modifyIndexHtmlResponse(indexHtmlResponse);
204204

205+
addWatermark(service.getDeploymentConfiguration(), indexDocument);
206+
205207
try {
206208
response.getOutputStream()
207209
.write(indexDocument.html().getBytes(UTF_8));
@@ -632,6 +634,23 @@ private static void modifyIndexHtmlForVite(Document indexHtmlDocument) {
632634
"<script type='text/javascript'>window.JSCompiler_renameProperty = function(a) { return a;}</script>");
633635
}
634636

637+
private static void addWatermark(DeploymentConfiguration config,
638+
Document indexDocument) {
639+
System.clearProperty("vaadin." + Constants.WATERMARK_TOKEN);
640+
if (config.isProductionMode() && config
641+
.getBooleanProperty(Constants.WATERMARK_TOKEN, false)) {
642+
Element elm = new Element(SCRIPT);
643+
elm.attr(SCRIPT_INITIAL, "");
644+
elm.attr("type", "module");
645+
elm.appendChild(new DataNode(
646+
"""
647+
const root = document.body.attachShadow({ mode: 'closed' })
648+
root.innerHTML = '<slot></slot><vaadin-watermark></vaadin-watermark>'
649+
"""));
650+
indexDocument.head().insertChildren(0, elm);
651+
}
652+
}
653+
635654
// Holds parsed index.html to avoid re-parsing on every request in
636655
// production mode
637656
//

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ private static boolean needsBuildInternal(Options options,
233233
((ObjectNode) statsJson.get(FRONTEND_HASHES_STATS_KEY))
234234
.remove(FrontendUtils.INDEX_HTML);
235235

236+
if (isWatermarkConditionChanged(options, statsJson)) {
237+
UsageStatistics.markAsUsed(
238+
"flow/rebundle-reason-watermark-condition-changed", null);
239+
return true;
240+
}
241+
// watermark file hash has already been checked, if needed.
242+
// removing it from hashes map to prevent other unnecessary checks
243+
((ObjectNode) statsJson.get(FRONTEND_HASHES_STATS_KEY))
244+
.remove(FrontendUtils.GENERATED + FrontendUtils.WATERMARK_JS);
245+
236246
if (!BundleValidationUtil.frontendImportsFound(statsJson, options,
237247
frontendDependencies)) {
238248
UsageStatistics.markAsUsed(
@@ -755,6 +765,42 @@ private static boolean indexFileAddedOrDeleted(Options options,
755765
return false;
756766
}
757767

768+
private static boolean isWatermarkConditionChanged(Options options,
769+
JsonNode statsJson) throws IOException {
770+
final JsonNode frontendHashes = statsJson
771+
.get(FRONTEND_HASHES_STATS_KEY);
772+
boolean watermarkRequested = options.isWatermarkEnable();
773+
String watermarkPath = FrontendUtils.GENERATED
774+
+ FrontendUtils.WATERMARK_JS;
775+
boolean hasWatermarkHash = frontendHashes.has(watermarkPath);
776+
if (!watermarkRequested && hasWatermarkHash) {
777+
getLogger().info(
778+
"Detected watermark file but watermark is not enabled");
779+
return true;
780+
}
781+
if (!options.isProductionMode() && hasWatermarkHash) {
782+
getLogger().info(
783+
"Detected watermark file but watermark is applied in development bundle");
784+
return true;
785+
}
786+
if (watermarkRequested && options.isProductionMode()) {
787+
if (!hasWatermarkHash) {
788+
getLogger().info("Detected missing watermark file");
789+
return true;
790+
}
791+
792+
List<String> faultyContent = new ArrayList<>();
793+
compareFrontendHashes(frontendHashes, faultyContent, watermarkPath,
794+
new TaskGenerateWatermarkComponent(options)
795+
.getFileContent());
796+
if (!faultyContent.isEmpty()) {
797+
getLogger().info("Detected changed content for watermark file");
798+
return true;
799+
}
800+
}
801+
return false;
802+
}
803+
758804
private static Map<String, String> getRemainingImports(
759805
List<String> jarImports, List<String> projectImports,
760806
JsonNode frontendHashes) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ public class FrontendUtils {
241241
*/
242242
public static final String VITE_DEVMODE_TS = "vite-devmode.ts";
243243

244+
public static final String WATERMARK_JS = "watermark.js";
245+
244246
public static final String ROUTES_TS = "routes.ts";
245247

246248
public static final String ROUTES_TSX = "routes.tsx";

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public class NodeTasks implements FallibleCommand {
6666
TaskGenerateReactFiles.class,
6767
TaskUpdateOldIndexTs.class,
6868
TaskGenerateViteDevMode.class,
69+
TaskGenerateWatermarkComponent.class,
6970
TaskGenerateTsConfig.class,
7071
TaskGenerateTsDefinitions.class,
7172
TaskGenerateServiceWorker.class,
@@ -140,6 +141,7 @@ public NodeTasks(Options options) {
140141
"flow/prod-pre-compiled-bundle", null);
141142
}
142143
} else {
144+
commands.add(new TaskGenerateWatermarkComponent(options));
143145
BundleUtils.copyPackageLockFromBundle(options);
144146
}
145147
} else if (options.isBundleBuild()) {

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ public class Options implements Serializable {
139139

140140
private boolean frontendIgnoreVersionChecks = false;
141141

142+
private boolean watermarkEnable = false;
143+
142144
/**
143145
* Creates a new instance.
144146
*
@@ -1018,6 +1020,29 @@ public Options withFrontendDependenciesScanner(
10181020
return this;
10191021
}
10201022

1023+
/**
1024+
* Checks if the watermark build is enabled.
1025+
*
1026+
* @return {@code true} if the watermark build is enabled, {@code false}
1027+
* otherwise
1028+
*/
1029+
public boolean isWatermarkEnable() {
1030+
return watermarkEnable;
1031+
}
1032+
1033+
/**
1034+
* Sets whether the build should generate a watermarked application.
1035+
*
1036+
* @param watermarkEnable
1037+
* a boolean value indicating whether the built application
1038+
* should be watermarked.
1039+
* @return this builder
1040+
*/
1041+
public Options withWatermarkEnable(boolean watermarkEnable) {
1042+
this.watermarkEnable = watermarkEnable;
1043+
return this;
1044+
}
1045+
10211046
/**
10221047
* Gets the frontend dependencies scanner to use. If not is not pre-set,
10231048
* this initializes a new one based on the Options set.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.server.frontend;
17+
18+
import java.io.File;
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
22+
import org.apache.commons.io.IOUtils;
23+
24+
import static java.nio.charset.StandardCharsets.UTF_8;
25+
26+
/**
27+
* Generate <code>watermarks.js</code> if it is missing in frontend/generated
28+
* folder.
29+
* <p>
30+
* For internal use only. May be renamed or removed in a future release.
31+
*
32+
* @since 24.9
33+
*/
34+
public class TaskGenerateWatermarkComponent
35+
extends AbstractTaskClientGenerator {
36+
37+
private Options options;
38+
39+
/**
40+
* Create a task to generate <code>watermark.js</code> if necessary.
41+
*
42+
* @param options
43+
* the task options
44+
*/
45+
TaskGenerateWatermarkComponent(Options options) {
46+
this.options = options;
47+
}
48+
49+
@Override
50+
protected File getGeneratedFile() {
51+
return new File(new File(options.getFrontendDirectory(),
52+
FrontendUtils.GENERATED), FrontendUtils.WATERMARK_JS);
53+
}
54+
55+
@Override
56+
protected boolean shouldGenerate() {
57+
return options.isProductionMode() && options.isBundleBuild()
58+
&& options.isWatermarkEnable();
59+
}
60+
61+
@Override
62+
protected String getFileContent() throws IOException {
63+
try (InputStream content = getClass()
64+
.getResourceAsStream(FrontendUtils.WATERMARK_JS)) {
65+
return IOUtils.toString(content, UTF_8);
66+
}
67+
}
68+
69+
}

flow-server/src/main/java/com/vaadin/flow/server/startup/AbstractConfigurationFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import static com.vaadin.flow.server.Constants.PREMIUM_FEATURES;
4747
import static com.vaadin.flow.server.Constants.PROJECT_FRONTEND_GENERATED_DIR_TOKEN;
4848
import static com.vaadin.flow.server.Constants.VAADIN_PREFIX;
49+
import static com.vaadin.flow.server.Constants.WATERMARK_TOKEN;
4950
import static com.vaadin.flow.server.InitParameters.APPLICATION_IDENTIFIER;
5051
import static com.vaadin.flow.server.InitParameters.BUILD_FOLDER;
5152
import static com.vaadin.flow.server.InitParameters.FRONTEND_HOTDEPLOY;
@@ -198,6 +199,11 @@ protected Map<String, String> getConfigParametersUsingTokenData(
198199
buildInfo.get(NPM_EXCLUDE_WEB_COMPONENTS).booleanValue()));
199200
}
200201

202+
if (buildInfo.has(WATERMARK_TOKEN)) {
203+
params.put(WATERMARK_TOKEN, String
204+
.valueOf(buildInfo.get(WATERMARK_TOKEN).booleanValue()));
205+
}
206+
201207
setDevModePropertiesUsingTokenData(params, buildInfo);
202208
return params;
203209
}

0 commit comments

Comments
 (0)