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: run Vaadin plugin as build step #1171

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
40 changes: 39 additions & 1 deletion .github/workflows/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,51 @@ jobs:
with:
name: tests-output-it-prod
path: tests-report-*.tgz
e2e-embedded-plugin-tests:
name: End-to-end tests (Embedded Vaadin Plugin)
needs: [changes]
if: ${{ needs.changes.outputs.validation-required == 'true' }}
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- uses: browser-actions/setup-chrome@latest
id: setup-chrome
if: ${{ !vars.QH_DISABLE_CHROME_INSTALL }}
with:
chrome-version: stable
- name: Build
run: |
set -x -e -o pipefail
mvn -V -e -B -ntp -DskipTests -Dmaven.javadoc.skip=false install
- name: End-to-end Test (Production mode)
run: |
set -x -e -o pipefail
mvn -V -e -B -ntp verify -Dmaven.javadoc.skip=false -DtrimStackTrace=false -Dselenide.browserBinary=${{ steps.setup-chrome.outputs.chrome-path }} -Pit-tests,production,embedded-plugin -Dvaadin.build.enabled=true
- name: Package test output files
if: ${{ failure() || success() }}
run: find . -name surefire-reports -o -name failsafe-reports -o -name selenide-reports | tar -czf tests-report-e2e-embedded-plugin.tgz -T -
- uses: actions/upload-artifact@v4
if: ${{ failure() || success() }}
with:
name: tests-output-it-embedded-plugin
path: tests-report-*.tgz
test-results:
permissions:
issues: read
checks: write
pull-requests: write
if: ${{ always() }}
needs: [changes, build-and-test, e2e-dev-tests, e2e-prod-tests]
needs: [changes, build-and-test, e2e-dev-tests, e2e-prod-tests, e2e-embedded-plugin-tests]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
13 changes: 13 additions & 0 deletions commons/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@
<artifactId>vaadin-quarkus-deployment</artifactId>
<version>${vaadin.quarkus.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>flow-plugin-base</artifactId>
<version>${flow.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-dev-bundle</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-prod-bundle</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.builder.BuildException;
import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
Expand Down Expand Up @@ -98,6 +100,9 @@
import com.github.mcollovati.quarkus.hilla.QuarkusVaadinServiceListenerPropagator;
import com.github.mcollovati.quarkus.hilla.crud.FilterableRepositorySupport;
import com.github.mcollovati.quarkus.hilla.deployment.asm.OffendingMethodCallsReplacer;
import com.github.mcollovati.quarkus.hilla.deployment.vaadinplugin.QuarkusPluginAdapter;
import com.github.mcollovati.quarkus.hilla.deployment.vaadinplugin.VaadinBuildTimeConfig;
import com.github.mcollovati.quarkus.hilla.deployment.vaadinplugin.VaadinPlugin;
import com.github.mcollovati.quarkus.hilla.graal.DelayedInitBroadcaster;
import com.github.mcollovati.quarkus.hilla.reload.HillaLiveReloadRecorder;

Expand Down Expand Up @@ -520,6 +525,32 @@ void registerVaadinQuarkusServices(BuildProducer<AdditionalBeanBuildItem> produc
.build());
}

@BuildStep(onlyIf = IsNormal.class)
void buildFrontendTask(
CurateOutcomeBuildItem outcomeBuildItem,
VaadinBuildTimeConfig vaadinConfig,
CombinedIndexBuildItem indexBuildItem,
BuildProducer<GeneratedResourceBuildItem> producer)
throws BuildException {
if (vaadinConfig.enabled()) {
VaadinPlugin vaadinPlugin = new VaadinPlugin(vaadinConfig, outcomeBuildItem.getApplicationModel());
vaadinPlugin.prepareFrontend();
vaadinPlugin.buildFrontend(indexBuildItem.getComputingIndex());
}
}

public static final class VaadinPluginBuildItem extends SimpleBuildItem {
private final QuarkusPluginAdapter plugin;

public VaadinPluginBuildItem(QuarkusPluginAdapter plugin) {
this.plugin = plugin;
}

public QuarkusPluginAdapter getPlugin() {
return plugin;
}
}

public static final class NavigationAccessControlBuildItem extends SimpleBuildItem {

private final String loginPath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,36 @@
*/
package com.github.mcollovati.quarkus.hilla.deployment.asm;

import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import com.vaadin.hilla.ApplicationContextProvider;
import com.vaadin.hilla.AuthenticationUtil;
import com.vaadin.hilla.EndpointCodeGenerator;
import com.vaadin.hilla.EndpointInvoker;
import com.vaadin.hilla.EndpointRegistry;
import com.vaadin.hilla.EndpointUtil;
import com.vaadin.hilla.Hotswapper;
import com.vaadin.hilla.engine.EngineConfiguration;
import com.vaadin.hilla.parser.utils.ConfigList;
import com.vaadin.hilla.push.PushEndpoint;
import com.vaadin.hilla.push.PushMessageHandler;
import com.vaadin.hilla.signals.core.registry.SecureSignalsRegistry;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
import io.quarkus.gizmo.BranchResult;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.ClassTransformer;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import org.objectweb.asm.Opcodes;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Sort;

import com.github.mcollovati.quarkus.hilla.HillaReplacements;
import com.github.mcollovati.quarkus.hilla.SpringReplacements;

public class OffendingMethodCallsReplacer {
Expand Down Expand Up @@ -106,6 +119,8 @@ public static void addClassVisitors(BuildProducer<BytecodeTransformerBuildItem>
transformer.removeMethod(sortMethod);
return transformer.applyTo(classVisitor);
}));
producer.produce(applicationContextProvider_runOnContext_patch());
producer.produce(endpointCodeGenerator_findBrowserCallables_replacement());
}

@SafeVarargs
Expand All @@ -116,4 +131,58 @@ private static BytecodeTransformerBuildItem transform(
(s, classVisitor) ->
new MethodReplacementClassVisitor(classVisitor, method, Map.ofEntries(replacements)));
}

private static BytecodeTransformerBuildItem applicationContextProvider_runOnContext_patch() {
return new BytecodeTransformerBuildItem(
ApplicationContextProvider.class.getName(), (className, classVisitor) -> {
ClassTransformer transformer = new ClassTransformer(className);
MethodDescriptor runOnContextMethod = MethodDescriptor.ofMethod(
ApplicationContextProvider.class, "runOnContext", void.class, Consumer.class);
transformer.removeMethod(runOnContextMethod);
try (MethodCreator creator = transformer.addMethod(runOnContextMethod)) {
creator.setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC);
ResultHandle appCtxField = creator.readStaticField(FieldDescriptor.of(
ApplicationContextProvider.class, "applicationContext", ApplicationContext.class));
ResultHandle pendingActionsField = creator.readStaticField(
FieldDescriptor.of(ApplicationContextProvider.class, "pendingActions", List.class));
BranchResult ifNullAppCtx = creator.ifNull(appCtxField);
try (BytecodeCreator trueBranch = ifNullAppCtx.trueBranch()) {
trueBranch.invokeInterfaceMethod(
MethodDescriptor.ofMethod(List.class, "add", boolean.class, Object.class),
pendingActionsField,
creator.getMethodParam(0));
}
try (BytecodeCreator falseBranch = ifNullAppCtx.falseBranch()) {
falseBranch.invokeInterfaceMethod(
MethodDescriptor.ofMethod(Consumer.class, "accept", void.class, Object.class),
creator.getMethodParam(0),
appCtxField);
}
creator.returnVoid();
}
return transformer.applyTo(classVisitor);
});
}

private static BytecodeTransformerBuildItem endpointCodeGenerator_findBrowserCallables_replacement() {
return new BytecodeTransformerBuildItem(EndpointCodeGenerator.class.getName(), (className, classVisitor) -> {
ClassTransformer transformer = new ClassTransformer(className);
MethodDescriptor findBrowserCallablesMethod = MethodDescriptor.ofMethod(
className, "findBrowserCallables", List.class, EngineConfiguration.class, ApplicationContext.class);
transformer.removeMethod(findBrowserCallablesMethod);
try (MethodCreator creator = transformer.addMethod(findBrowserCallablesMethod)) {
creator.setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_PUBLIC);
creator.returnValue(creator.invokeStaticMethod(
MethodDescriptor.ofMethod(
HillaReplacements.class,
"findBrowserCallables",
List.class,
EngineConfiguration.class,
ApplicationContext.class),
creator.getMethodParam(0),
creator.getMethodParam(1)));
}
return transformer.applyTo(classVisitor);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2025 Marco Collovati, Dario Götze
*
* 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.github.mcollovati.quarkus.hilla.deployment.vaadinplugin;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

import com.vaadin.flow.server.ExecutionFailedException;
import com.vaadin.hilla.engine.EngineConfiguration;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.CompositeIndex;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.JarIndexer;
import org.jboss.jandex.Result;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class QuarkusHillaBrowserCallableFinder implements EngineConfiguration.BrowserCallableFinder {

private static final Logger LOGGER = LoggerFactory.getLogger(QuarkusHillaBrowserCallableFinder.class);

private IndexView index;

public QuarkusHillaBrowserCallableFinder() {}

public QuarkusHillaBrowserCallableFinder(IndexView index) {
this.index = index;
}

@Override
public List<Class<?>> findBrowserCallables() throws ExecutionFailedException {
EngineConfiguration configuration = EngineConfiguration.getDefault();
IndexView compositeIndex = getOrCreateIndex(configuration);
Set<String> browserCallables = configuration.getEndpointAnnotations().stream()
.map(DotName::createSimple)
.flatMap(ann -> compositeIndex.getAnnotations(ann).stream())
.filter(instance -> instance.target().kind() == AnnotationTarget.Kind.CLASS)
.map(instance -> instance.target().asClass().name().toString())
.collect(Collectors.toSet());
List<Class<?>> browserCallableClasses = new ArrayList<>();
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
for (String browserCallable : browserCallables) {
try {
browserCallableClasses.add(Class.forName(browserCallable, false, contextClassLoader));
LOGGER.debug("Found browser callable {}", browserCallable);
} catch (ClassNotFoundException e) {
LOGGER.error(
"Cannot load browser callable class {} using {} class loader",
browserCallable,
contextClassLoader,
e);
}
}
return browserCallableClasses;
}

private IndexView getOrCreateIndex(EngineConfiguration configuration) throws ExecutionFailedException {
if (index == null) {
Path tempFile;
try {
tempFile = Files.createTempFile("jandex", "idx");
} catch (IOException e) {
throw new ExecutionFailedException(e);
}
tempFile.toFile().deleteOnExit();
List<IndexView> indexes = new ArrayList<>();
try {
for (Path path : configuration.getClasspath()) {
LOGGER.trace("Indexing {}", path);
Indexer indexer = new Indexer();
File file = path.toFile();
if (file.isDirectory()) {
Files.walkFileTree(path, new DirectoryIndexer(indexer));
indexes.add(indexer.complete());
} else if (file.exists()
&& file.getName().toLowerCase(Locale.ROOT).endsWith(".jar")) {
Result result =
JarIndexer.createJarIndex(file, indexer, tempFile.toFile(), false, false, false);
indexes.add(result.getIndex());
}
}
} catch (IOException e) {
throw new ExecutionFailedException(e);
}
index = CompositeIndex.create(indexes);
}
return index;
}

private static class DirectoryIndexer extends SimpleFileVisitor<Path> {

private final Indexer indexer;

public DirectoryIndexer(Indexer indexer) {
this.indexer = indexer;
}

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.getFileName().toString().endsWith(".class")) {
try (InputStream stream = Files.newInputStream(file)) {
indexer.index(stream);
}
}
return FileVisitResult.CONTINUE;
}
}
}
Loading
Loading