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

feat: add endpoint detection #20567

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.frontend.EndpointGeneratorTaskFactory;
import com.vaadin.flow.server.frontend.EndpointUsageDetector;
import com.vaadin.flow.server.frontend.FrontendTools;
import com.vaadin.flow.server.frontend.FrontendUtils;
import com.vaadin.flow.server.frontend.installer.NodeInstaller;
import com.vaadin.flow.server.frontend.scanner.ClassFinder;

import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;
Expand Down Expand Up @@ -206,6 +206,9 @@ public void setup() throws Exception {
TestUtils.getInitialPackageJson().toJson());

lookup = Mockito.mock(Lookup.class);

Mockito.doReturn(new TestEndpointUsageDetector()).when(lookup)
.lookup(EndpointUsageDetector.class);
Mockito.doReturn(new TestEndpointGeneratorTaskFactory()).when(lookup)
.lookup(EndpointGeneratorTaskFactory.class);
Mockito.doAnswer(invocation -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.vaadin.flow.plugin.maven;

import com.vaadin.flow.server.frontend.EndpointUsageDetector;
import com.vaadin.flow.server.frontend.Options;

public class TestEndpointUsageDetector implements EndpointUsageDetector {

@Override
public boolean areEndpointsUsed(Options options) {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.InitParameters;
import com.vaadin.flow.server.frontend.EndpointGeneratorTaskFactory;
import com.vaadin.flow.server.frontend.EndpointUsageDetector;
import com.vaadin.flow.server.frontend.FileIOUtils;
import com.vaadin.flow.server.frontend.FrontendTools;
import com.vaadin.flow.server.frontend.FrontendUtils;
Expand All @@ -52,7 +53,6 @@
import com.vaadin.flow.utils.LookupImpl;
import com.vaadin.pro.licensechecker.LicenseChecker;
import com.vaadin.pro.licensechecker.Product;

import elemental.json.Json;
import elemental.json.JsonObject;
import elemental.json.impl.JsonUtil;
Expand Down Expand Up @@ -135,6 +135,13 @@ public void should_useHillaEngine_withNodeUpdater()
MockedConstruction<TaskRunNpmInstall> construction = Mockito
.mockConstruction(TaskRunNpmInstall.class);

final EndpointUsageDetector endpointUsageDetector = Mockito
.mock(EndpointUsageDetector.class);
Mockito.doReturn(true).when(endpointUsageDetector)
.areEndpointsUsed(Mockito.any());
Mockito.doReturn(endpointUsageDetector).when(lookup)
.lookup(EndpointUsageDetector.class);

final EndpointGeneratorTaskFactory endpointGeneratorTaskFactory = Mockito
.mock(EndpointGeneratorTaskFactory.class);
Mockito.doReturn(endpointGeneratorTaskFactory).when(lookup)
Expand All @@ -158,6 +165,7 @@ public void should_useHillaEngine_withNodeUpdater()
BuildFrontendUtil.runNodeUpdater(adapter);
}

Mockito.verify(lookup).lookup(EndpointUsageDetector.class);
Mockito.verify(lookup).lookup(EndpointGeneratorTaskFactory.class);
Mockito.verify(lookup, Mockito.never())
.lookup(TaskGenerateOpenAPI.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

import jakarta.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;

import com.vaadin.flow.server.frontend.scanner.ClassFinder;

Expand All @@ -33,6 +36,10 @@ public interface EndpointRequestUtil extends Serializable {

String HILLA_ENDPOINT_CLASS = "com.vaadin.hilla.EndpointController";

String HILLA_ENDPOINT_ANNOTATION = "com.vaadin.hilla.Endpoint";

String HILLA_BROWSER_CALLABLE_ANNOTATION = "com.vaadin.hilla.BrowserCallable";

/**
* Checks if the request is for an endpoint.
* <p>
Expand Down Expand Up @@ -86,4 +93,19 @@ static boolean isHillaAvailable(ClassFinder classFinder) {
return false;
}
}

static List<Class<? extends Annotation>> getHillaEndpointAnnotations() {
List<Class<? extends Annotation>> result = new ArrayList<>();
try {
if (isHillaAvailable()) {
result.add((Class<? extends Annotation>) Class
.forName(HILLA_ENDPOINT_ANNOTATION));
result.add((Class<? extends Annotation>) Class
.forName(HILLA_BROWSER_CALLABLE_ANNOTATION));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.flow.server.frontend;

/**
* Detector for Vaadin Endpoint usage.
* <p>
* For internal use only. May be renamed or removed in a future release.
*
* @author Vaadin Ltd.
*/
public interface EndpointUsageDetector {

/**
* Checks whether endpoints are used in the project.
*
* @param options
* task options for endpoint generator task
* @return {@literal true} if endpoints are used. {@literal false}
* otherwise.
*/
boolean areEndpointsUsed(Options options);

}
Original file line number Diff line number Diff line change
Expand Up @@ -1380,6 +1380,22 @@ public static boolean isHillaUsed(File frontendDirectory,
&& isHillaViewsUsed(frontendDirectory);
}

/**
* Checks if Hilla's endpoints, i.e. {@code BrowserCallable} or
* {@code Endpoint} annotated classes are used in the project.
*
* @param options
* frontend build options
* @return {@code true} if Hilla endpoints are used, {@code false} otherwise
*/
public static boolean areEndpointsUsed(Options options) {
mshabarov marked this conversation as resolved.
Show resolved Hide resolved
Lookup lookup = options.getLookup();
EndpointUsageDetector endpointUsageDetector = lookup
.lookup(EndpointUsageDetector.class);
return endpointUsageDetector != null
&& endpointUsageDetector.areEndpointsUsed(options);
}

private static boolean isRoutesContentUsingHillaViews(
String routesContent) {
routesContent = StringUtil.removeComments(routesContent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,7 @@ private void addGenerateServiceWorkerTask(Options options,
}

private void addEndpointServicesTasks(Options options) {
if (!FrontendUtils.isHillaUsed(options.getFrontendDirectory(),
options.getClassFinder())) {
if (!FrontendUtils.areEndpointsUsed(options)) {
return;
}
Lookup lookup = options.getLookup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,8 @@ private JsonObject generateVersionsFromPackageJson(JsonObject packageJson) {
private void putHillaComponentsDependencies(
Map<String, String> dependencies, String packageJsonKey) {
if (FrontendUtils.isHillaUsed(options.getFrontendDirectory(),
options.getClassFinder())) {
options.getClassFinder())
|| FrontendUtils.areEndpointsUsed(options)) {
if (options.isReactEnabled()) {
dependencies.putAll(readDependenciesIfAvailable(
"hilla/components/react", packageJsonKey));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import com.vaadin.flow.server.VaadinContext;
import com.vaadin.flow.server.VaadinServletContext;
import com.vaadin.flow.server.frontend.EndpointGeneratorTaskFactory;
import com.vaadin.flow.server.frontend.EndpointUsageDetector;

/**
* Standard servlet initializer for collecting all SPI implementations.
Expand All @@ -55,7 +56,7 @@
*/
@HandlesTypes({ ResourceProvider.class, InstantiatorFactory.class,
DeprecatedPolymerPublishedEventHandler.class,
EndpointGeneratorTaskFactory.class,
EndpointGeneratorTaskFactory.class, EndpointUsageDetector.class,
ApplicationConfigurationFactory.class, AbstractLookupInitializer.class,
AppShellPredicate.class, StaticFileHandlerFactory.class,
DevModeHandlerManager.class, BrowserLiveReloadAccessor.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ public class NodeTasksHillaTest {
@Mock
private TaskGenerateEndpoint taskGenerateEndpoint;

@Mock
private EndpointUsageDetector endpointUsageDetector;

@Before
public void setup() throws Exception {
userDir = temporaryFolder.getRoot().getAbsolutePath();
Expand Down Expand Up @@ -106,7 +109,7 @@ private Options createOptions() {
}

@Test
public void should_useHillaEngine_whenEnabled()
public void should_useHillaEngine_whenEndpointsAreDetected()
throws ExecutionFailedException, IOException {
Options options = createOptions();
Mockito.doReturn(taskGenerateOpenAPI).when(endpointGeneratorTaskFactory)
Expand All @@ -116,6 +119,10 @@ public void should_useHillaEngine_whenEnabled()
.createTaskGenerateEndpoint(any());
Mockito.doReturn(endpointGeneratorTaskFactory).when(options.getLookup())
.lookup(EndpointGeneratorTaskFactory.class);
Mockito.doReturn(true).when(endpointUsageDetector)
.areEndpointsUsed(any());
Mockito.doReturn(endpointUsageDetector).when(options.getLookup())
.lookup(EndpointUsageDetector.class);

try (MockedStatic<FrontendUtils> util = Mockito
.mockStatic(FrontendUtils.class, Mockito.CALLS_REAL_METHODS)) {
Expand All @@ -128,6 +135,33 @@ public void should_useHillaEngine_whenEnabled()
verifyHillaEngine(true);
}

@Test
public void should_notUseHillaEngine_whenEndpointsAreNotDetected()
throws ExecutionFailedException, IOException {
Options options = createOptions();
Mockito.doReturn(taskGenerateOpenAPI).when(endpointGeneratorTaskFactory)
.createTaskGenerateOpenAPI(any());
Mockito.doReturn(taskGenerateEndpoint)
.when(endpointGeneratorTaskFactory)
.createTaskGenerateEndpoint(any());
Mockito.doReturn(endpointGeneratorTaskFactory).when(options.getLookup())
.lookup(EndpointGeneratorTaskFactory.class);
Mockito.doReturn(false).when(endpointUsageDetector)
.areEndpointsUsed(any());
Mockito.doReturn(endpointUsageDetector).when(options.getLookup())
.lookup(EndpointUsageDetector.class);

try (MockedStatic<FrontendUtils> util = Mockito
.mockStatic(FrontendUtils.class, Mockito.CALLS_REAL_METHODS)) {
util.when(() -> FrontendUtils.isHillaUsed(Mockito.any(),
Mockito.any())).thenReturn(true);

new NodeTasks(options).execute();
}

verifyHillaEngine(false);
}

@Test
public void should_notHillaEngine_whenDisabled()
throws ExecutionFailedException, IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,37 @@ public void getDefaultDependencies_hillaIsNotUsed_doesntAddHillaComponents() {
defaultDevDeps.containsKey("lit-dev-dependency"));
}

@Test
public void getDefaultDependencies_endpointsAreUsed_addsHillaPackages() {
try (MockedStatic<FrontendUtils> utilsMock = Mockito
.mockStatic(FrontendUtils.class)) {
utilsMock.when(
() -> FrontendUtils.isHillaUsed(Mockito.any(File.class),
Mockito.any(ClassFinder.class)))
.thenReturn(false);

utilsMock
.when(() -> FrontendUtils
.areEndpointsUsed(Mockito.any(Options.class)))
.thenReturn(false);
Map<String, String> defaultDeps = nodeUpdater
.getDefaultDependencies();
Assert.assertFalse(
"Hilla dev dependency added unexpectedly when no endpoints are used",
defaultDeps.containsKey("react-dev-dependency"));

utilsMock
.when(() -> FrontendUtils
.areEndpointsUsed(Mockito.any(Options.class)))
.thenReturn(true);
Map<String, String> defaultDevDeps = nodeUpdater
.getDefaultDevDependencies();
Assert.assertTrue(
"Hilla dev dependency is expected when endpoints are used",
defaultDevDeps.containsKey("react-dev-dependency"));
}
}

@Test
public void readPackageJson_nonExistingFile_doesNotThrow()
throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ protected Stream<String> getExcludedPatterns() {
"com\\.vaadin\\.flow\\.server\\.frontend\\.Task.*",
"com\\.vaadin\\.flow\\.server\\.frontend\\.AbstractTaskClientGenerator",
"com\\.vaadin\\.flow\\.server\\.frontend\\.EndpointGeneratorTaskFactory",
"com\\.vaadin\\.flow\\.server\\.frontend\\.EndpointUsageDetector",
"com\\.vaadin\\.flow\\.server\\.frontend\\.CvdlProducts",
"com\\.vaadin\\.flow\\.server\\.frontend\\.GenerateMainImports",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import com.vaadin.experimental.FeatureFlags;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.internal.DevModeHandler;
import com.vaadin.flow.internal.hilla.EndpointRequestUtil;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.flow.server.InitParameters;
Expand Down Expand Up @@ -129,8 +130,11 @@ private void ensureImplementation(Class<?> clazz) {
private static Set<String> calculateApplicableClassNames() {
HandlesTypes handlesTypes = DevModeStartupListener.class
.getAnnotation(HandlesTypes.class);
return Stream.of(handlesTypes.value()).map(Class::getName)
.collect(Collectors.toSet());
return Stream
.concat(Stream.of(handlesTypes.value()),
EndpointRequestUtil.getHillaEndpointAnnotations()
.stream())
.map(Class::getName).collect(Collectors.toSet());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.HandlesTypes;
import jakarta.servlet.annotation.WebListener;

import java.io.Serializable;
import java.util.Set;

Expand Down
Loading
Loading