Skip to content

Commit 716a835

Browse files
committed
feat: Improve license error for unoptimized bundle
When `optimizeBundle` is disabled the bytecode scanner is skipped and every commercial component on the classpath is treated as used, so the existing license error wrongly states "Your application contains the following commercial components" even when the application does not reference any of them. This is common for projects that pull the `com.vaadin:vaadin` umbrella artifact directly or transitively. Detect the disabled `optimizeBundle` case in `BuildFrontendUtil.validateLicenses` and throw a `LicenseException` whose message explains the classpath-level detection and gives concrete workarounds: re-enable `optimizeBundle`, replace `com.vaadin:vaadin` with `com.vaadin:vaadin-core` when declared as a direct dependency, or identify and exclude the transitive pull via `mvn dependency:tree` / `./gradlew dependencyInsight`. A new `validateLicenses(PluginAdapterBuild, ...)` overload reads the flag; the `PluginAdapterBase` overload is deprecated and delegates to it. Fixes #24051
1 parent 748ff8a commit 716a835

2 files changed

Lines changed: 197 additions & 14 deletions

File tree

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

Lines changed: 109 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,37 @@ private static void runFrontendBuildTool(PluginAdapterBase adapter,
605605
}
606606
}
607607

608+
/**
609+
* Validate pro component licenses.
610+
* <p>
611+
* When {@link PluginAdapterBuild#optimizeBundle()} is {@code false} the
612+
* bytecode scanner is disabled and every commercial component present on
613+
* the classpath is assumed to be used, regardless of whether the
614+
* application actually references it. In that case the thrown
615+
* {@link LicenseException} uses a dedicated message that explains the
616+
* classpath-level detection and suggests concrete workarounds (re-enable
617+
* bundle optimization, switch to {@code com.vaadin:vaadin-core}, or exclude
618+
* {@code com.vaadin:vaadin} from a transitive dependency).
619+
*
620+
* @param adapter
621+
* the PluginAdapterBuild
622+
* @param frontendDependencies
623+
* frontend dependencies scanner
624+
* @return {@literal true} if license validation is required because of the
625+
* presence of commercial components, otherwise {@literal false}.
626+
* @throws MissingLicenseKeyException
627+
* if commercial components are used in a commercial
628+
* banner-enabled build and no license key is present
629+
* @throws LicenseException
630+
* if commercial components are used without a license and
631+
* commercial banner is not enabled
632+
*/
633+
public static boolean validateLicenses(PluginAdapterBuild adapter,
634+
FrontendDependenciesScanner frontendDependencies) {
635+
return doValidateLicenses(adapter, frontendDependencies,
636+
adapter.optimizeBundle());
637+
}
638+
608639
/**
609640
* Validate pro component licenses.
610641
*
@@ -620,9 +651,25 @@ private static void runFrontendBuildTool(PluginAdapterBase adapter,
620651
* @throws LicenseException
621652
* if commercial components are used without a license and
622653
* commercial banner is not enabled
654+
* @deprecated use
655+
* {@link #validateLicenses(PluginAdapterBuild, FrontendDependenciesScanner)}
656+
* instead
623657
*/
658+
@Deprecated(since = "25.2", forRemoval = true)
624659
public static boolean validateLicenses(PluginAdapterBase adapter,
625660
FrontendDependenciesScanner frontendDependencies) {
661+
if (adapter instanceof PluginAdapterBuild pluginAdapterBuild) {
662+
return validateLicenses(pluginAdapterBuild, frontendDependencies);
663+
}
664+
// Fallback for callers that only provide a PluginAdapterBase: the
665+
// optimizeBundle property is not accessible, so assume the default
666+
// (true) and emit the original error message.
667+
return doValidateLicenses(adapter, frontendDependencies, true);
668+
}
669+
670+
private static boolean doValidateLicenses(PluginAdapterBase adapter,
671+
FrontendDependenciesScanner frontendDependencies,
672+
boolean optimizeBundle) {
626673
File outputFolder = adapter.frontendOutputDirectory();
627674

628675
String statsJsonContent = null;
@@ -679,20 +726,8 @@ public static boolean validateLicenses(PluginAdapterBase adapter,
679726
.formatted(productsList));
680727
}
681728
invalidateOutput(component, outputFolder);
682-
throw new LicenseException(String.format(
683-
"""
684-
Commercial features require a subscription.
685-
Your application contains the following commercial components and no license was found:
686-
%1$s
687-
688-
If you have an active subscription, please download the license key from https://vaadin.com/myaccount/licenses.
689-
Otherwise go to https://vaadin.com/pricing to obtain a license.
690-
691-
You can also build a watermarked version of the application configuring
692-
the '%2$s' property of the Maven or Gradle plugin
693-
or run the build with the '-Dvaadin.%2$s' system parameter
694-
""",
695-
productsList, InitParameters.COMMERCIAL_WITH_BANNER));
729+
throw new LicenseException(
730+
buildLicenseErrorMessage(productsList, optimizeBundle));
696731
} catch (Exception e) {
697732
invalidateOutput(component, outputFolder);
698733
throw e;
@@ -701,6 +736,66 @@ public static boolean validateLicenses(PluginAdapterBase adapter,
701736
return !commercialComponents.isEmpty();
702737
}
703738

739+
private static String buildLicenseErrorMessage(String productsList,
740+
boolean optimizeBundle) {
741+
if (optimizeBundle) {
742+
return String.format(
743+
"""
744+
Commercial features require a subscription.
745+
Your application contains the following commercial components and no license was found:
746+
%1$s
747+
748+
If you have an active subscription, please download the license key from https://vaadin.com/myaccount/licenses.
749+
Otherwise go to https://vaadin.com/pricing to obtain a license.
750+
751+
You can also build a watermarked version of the application configuring
752+
the '%2$s' property of the Maven or Gradle plugin
753+
or run the build with the '-Dvaadin.%2$s' system parameter
754+
""",
755+
productsList, InitParameters.COMMERCIAL_WITH_BANNER);
756+
}
757+
return String.format(
758+
"""
759+
Commercial features require a subscription.
760+
The following commercial components were detected on the classpath and no license was found:
761+
%1$s
762+
763+
If you have an active subscription, please download the license key from https://vaadin.com/myaccount/licenses.
764+
Otherwise go to https://vaadin.com/pricing to obtain a license.
765+
766+
You can also build a watermarked version of the application configuring
767+
the '%2$s' property of the Maven or Gradle plugin
768+
or run the build with the '-Dvaadin.%2$s' system parameter.
769+
770+
Note: bundle optimization is disabled ('optimizeBundle=false'), so the bytecode scanner that
771+
normally restricts detection to components actually referenced by the application is bypassed
772+
— every commercial component present on the classpath is assumed to be in use.
773+
774+
If the application does not actually use these commercial components, the likely cause is a
775+
dependency on the 'com.vaadin:vaadin' umbrella artifact, which transitively includes every
776+
commercial component. You can resolve this by:
777+
778+
1. Setting the 'optimizeBundle' property of the Vaadin plugin to 'true' (for example in the
779+
vaadin-maven-plugin configuration in pom.xml, the 'vaadin { }' block in build.gradle(.kts),
780+
or the corresponding entry in application.properties for Quarkus). Bytecode scanning will
781+
then restrict detection to components actually referenced by the application.
782+
783+
2. If your project declares 'com.vaadin:vaadin' as a direct dependency, replacing it
784+
with 'com.vaadin:vaadin-core'.
785+
786+
3. If 'com.vaadin:vaadin' is pulled in transitively (for example by an add-on), identify the
787+
responsible dependency by running:
788+
Maven: mvn dependency:tree -Dincludes=com.vaadin:vaadin
789+
Gradle: ./gradlew dependencyInsight --configuration runtimeClasspath --dependency com.vaadin:vaadin:
790+
(note the trailing colon after 'vaadin' — it narrows the match to the exact
791+
umbrella artifact and excludes 'com.vaadin:vaadin-*' sub-artifacts)
792+
Then exclude 'com.vaadin:vaadin' from that dependency.
793+
Please also report the issue to the add-on author — add-ons should declare 'com.vaadin:vaadin' with 'provided' scope
794+
in Maven or 'compileOnly' in Gradle so it is never leaked transitively to consumers.
795+
""",
796+
productsList, InitParameters.COMMERCIAL_WITH_BANNER);
797+
}
798+
704799
private static void invalidateOutput(Product component, File outputFolder) {
705800
try {
706801
getLogger().debug(

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

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ void setup() throws Exception {
107107
.thenReturn(new File(baseDir, "src/main/frontend/generated"));
108108
Mockito.when(adapter.projectBaseDirectory()).thenReturn(tmpDir);
109109
Mockito.when(adapter.applicationIdentifier()).thenReturn("TEST_APP_ID");
110+
Mockito.when(adapter.optimizeBundle()).thenReturn(true);
110111
ClassFinder classFinder = new ClassFinder.DefaultClassFinder(
111112
getClass().getClassLoader());
112113
lookup = Mockito.spy(new LookupImpl(classFinder));
@@ -628,6 +629,93 @@ void validateLicense_commercialFrontendProducts_noLocalKeys_buildWithCommercialB
628629
});
629630
}
630631

632+
@Test
633+
void validateLicense_commercialProducts_optimizeBundleDisabled_commercialBannerDisabled_failsWithOptimizeBundleAwareMessage()
634+
throws Exception {
635+
Mockito.when(adapter.isCommercialBannerEnabled()).thenReturn(false);
636+
Mockito.when(adapter.optimizeBundle()).thenReturn(false);
637+
638+
Files.createDirectories(statsJson.toPath().getParent());
639+
Map<String, String> packages = new HashMap<>();
640+
packages.put("comm-component", "4.6.5");
641+
packages.put("comm-component2", "4.6.5");
642+
packages.put("@vaadin/button", "1.2.1");
643+
644+
Set<String> commercialProducts = new HashSet<>();
645+
commercialProducts.add("comm-comp");
646+
commercialProducts.add("comm-comp2");
647+
648+
Files.writeString(statsJson.toPath(),
649+
statsJsonWithCommercialComponents());
650+
List<String> modules = List.of("comm-component/foo.js",
651+
"comm-component2/bar.js");
652+
Map<ChunkInfo, List<String>> modulesMap = Collections
653+
.singletonMap(ChunkInfo.GLOBAL, modules);
654+
655+
FrontendDependenciesScanner frontendDependencies = Mockito
656+
.mock(FrontendDependenciesScanner.class);
657+
Mockito.when(frontendDependencies.getPackages()).thenReturn(packages);
658+
Mockito.when(frontendDependencies.getModules()).thenReturn(modulesMap);
659+
660+
withMockedLicenseChecker(false, () -> {
661+
LicenseException exception = assertThrows(LicenseException.class,
662+
() -> BuildFrontendUtil.validateLicenses(adapter,
663+
frontendDependencies));
664+
commercialProducts.forEach(product -> assertTrue(
665+
exception.getMessage().contains(product),
666+
"Exception should list all commercial products but "
667+
+ product + " is missing"));
668+
String message = exception.getMessage();
669+
assertTrue(message.contains("optimizeBundle"),
670+
"Expected message to mention the optimizeBundle property but was: "
671+
+ message);
672+
assertTrue(message.contains("vaadin-core"),
673+
"Expected message to suggest switching to vaadin-core but was: "
674+
+ message);
675+
assertTrue(message.contains("provided"),
676+
"Expected message to mention provided scope for add-on authors but was: "
677+
+ message);
678+
assertTrue(message.contains(InitParameters.COMMERCIAL_WITH_BANNER),
679+
"Expected message to still suggest the commercial banner build but was: "
680+
+ message);
681+
assertFalse(adapter.frontendOutputDirectory().exists(),
682+
"Expected output directory to be deleted but was not");
683+
});
684+
}
685+
686+
@Test
687+
void validateLicense_commercialProducts_optimizeBundleDisabled_commercialBannerEnabled_propagateMissingKeyExceptionUnchanged()
688+
throws Exception {
689+
Mockito.when(adapter.isCommercialBannerEnabled()).thenReturn(true);
690+
Mockito.when(adapter.optimizeBundle()).thenReturn(false);
691+
692+
Files.createDirectories(statsJson.toPath().getParent());
693+
Map<String, String> packages = new HashMap<>();
694+
packages.put("comm-component", "4.6.5");
695+
packages.put("comm-component2", "4.6.5");
696+
packages.put("@vaadin/button", "1.2.1");
697+
698+
Files.writeString(statsJson.toPath(),
699+
statsJsonWithCommercialComponents());
700+
List<String> modules = List.of("comm-component/foo.js",
701+
"comm-component2/bar.js");
702+
Map<ChunkInfo, List<String>> modulesMap = Collections
703+
.singletonMap(ChunkInfo.GLOBAL, modules);
704+
705+
FrontendDependenciesScanner frontendDependencies = Mockito
706+
.mock(FrontendDependenciesScanner.class);
707+
Mockito.when(frontendDependencies.getPackages()).thenReturn(packages);
708+
Mockito.when(frontendDependencies.getModules()).thenReturn(modulesMap);
709+
710+
withMockedLicenseChecker(false, () -> {
711+
assertThrows(MissingLicenseKeyException.class,
712+
() -> BuildFrontendUtil.validateLicenses(adapter,
713+
frontendDependencies));
714+
assertTrue(adapter.frontendOutputDirectory().exists(),
715+
"Expected output directory to be preserved but was not");
716+
});
717+
}
718+
631719
private void withMockedLicenseChecker(boolean isValidLicense,
632720
ThrowingRunnable test) throws IOException {
633721
try (MockedStatic<LicenseChecker> licenseChecker = Mockito

0 commit comments

Comments
 (0)