Skip to content

Commit

Permalink
Merge pull request #39963 from phillip-kruger/mvnpm-webjar-locator
Browse files Browse the repository at this point in the history
Add mvnpm support in webjar-locator
  • Loading branch information
ia3andy authored Apr 10, 2024
2 parents 9597cd1 + fd1168a commit 7fd3cde
Show file tree
Hide file tree
Showing 14 changed files with 347 additions and 48 deletions.
13 changes: 13 additions & 0 deletions extensions/webjars-locator/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<!-- do not update these dependencies, they are only used for testing -->
<webjar.momentjs.version>2.24.0</webjar.momentjs.version>
<webjar.jquery-ui.version>1.13.0</webjar.jquery-ui.version>
<mvnpm.bootstrap.version>4.5.2</mvnpm.bootstrap.version>
</properties>

<dependencies>
Expand All @@ -31,6 +32,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-webjars-locator</artifactId>
</dependency>
<dependency>
<groupId>io.mvnpm</groupId>
<artifactId>importmap</artifactId>
</dependency>

<!-- Tests -->
<dependency>
Expand Down Expand Up @@ -63,6 +68,13 @@
<version>3.0.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>bootstrap</artifactId>
<version>${mvnpm.bootstrap.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-deployment</artifactId>
Expand Down Expand Up @@ -90,6 +102,7 @@
<systemPropertyVariables>
<webjar.jquery-ui.version>${webjar.jquery-ui.version}</webjar.jquery-ui.version>
<webjar.momentjs.version>${webjar.momentjs.version}</webjar.momentjs.version>
<mvnpm.bootstrap.version>${mvnpm.bootstrap.version}</mvnpm.bootstrap.version>
</systemPropertyVariables>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.webjar.locator.deployment;

import io.quarkus.builder.item.SimpleBuildItem;

public final class ImportMapBuildItem extends SimpleBuildItem {
private final String importmap;

public ImportMapBuildItem(String importmap) {
this.importmap = importmap;
}

public String getImportMap() {
return this.importmap;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.webjar.locator.deployment;

import java.util.Map;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;

/**
* Build time configuration for WebJar Locator.
*/
@ConfigRoot
public class WebJarLocatorConfig {

/**
* If the version reroute is enabled.
*/
@ConfigItem(defaultValue = "true")
public boolean versionReroute;

/**
* User defined import mappings
*/
@ConfigItem
public Map<String, String> importMappings;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.jboss.logging.Logger;

import io.mvnpm.importmap.Aggregator;
import io.quarkus.bootstrap.classloading.ClassPathElement;
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.deployment.Feature;
Expand All @@ -26,75 +30,142 @@
import io.quarkus.vertx.http.deployment.RouteBuildItem;
import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
import io.quarkus.webjar.locator.runtime.WebJarLocatorRecorder;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;

public class WebJarLocatorStandaloneBuildStep {

private static final String WEBJARS_PREFIX = "META-INF/resources/webjars";
private static final String WEBJARS_NAME = "webjars";

private static final String MVNPM_PREFIX = "META-INF/resources/_static";
private static final String MVNPM_NAME = "mvnpm";

private static final Logger log = Logger.getLogger(WebJarLocatorStandaloneBuildStep.class.getName());

@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
public void findWebjarsAndCreateHandler(
WebJarLocatorConfig config,
HttpBuildTimeConfig httpConfig,
BuildProducer<FeatureBuildItem> feature,
BuildProducer<RouteBuildItem> routes,
BuildProducer<ImportMapBuildItem> im,
CurateOutcomeBuildItem curateOutcome,
WebJarLocatorRecorder recorder) throws Exception {

final List<ClassPathElement> providers = QuarkusClassLoader.getElements(WEBJARS_PREFIX, false);
Map<String, String> webjarNameToVersionMap = Collections.emptyMap();
LibInfo webjarsLibInfo = getLibInfo(curateOutcome, WEBJARS_PREFIX, WEBJARS_NAME);
LibInfo mvnpmNameLibInfo = getLibInfo(curateOutcome, MVNPM_PREFIX, MVNPM_NAME);

if (webjarsLibInfo != null || mvnpmNameLibInfo != null) {
feature.produce(new FeatureBuildItem(Feature.WEBJARS_LOCATOR));

if (webjarsLibInfo != null) {
if (config.versionReroute) {
Handler<RoutingContext> handler = recorder.getHandler(getRootPath(httpConfig, "webjars"),
webjarsLibInfo.nameVersionMap);
routes.produce(RouteBuildItem.builder().route("/webjars/*").handler(handler).build());
}
} else {
log.warn(
"No WebJars were found in the project. Requests to the /webjars/ path will always return 404 (Not Found)");
}
if (mvnpmNameLibInfo != null) {
if (config.versionReroute) {
Handler<RoutingContext> handler = recorder.getHandler(getRootPath(httpConfig, "_static"),
mvnpmNameLibInfo.nameVersionMap);
routes.produce(RouteBuildItem.builder().route("/_static/*").handler(handler).build());
}
// Also create a importmap endpoint
Aggregator aggregator = new Aggregator(mvnpmNameLibInfo.jars);
if (!config.importMappings.isEmpty()) {
aggregator.addMappings(config.importMappings);
}

String importMap = aggregator.aggregateAsJson(false);
im.produce(new ImportMapBuildItem(importMap));
String path = getRootPath(httpConfig, IMPORTMAP_ROOT) + IMPORTMAP_FILENAME;
Handler<RoutingContext> importMapHandler = recorder.getImportMapHandler(path,
importMap);
routes.produce(
RouteBuildItem.builder().route("/" + IMPORTMAP_ROOT + "/" + IMPORTMAP_FILENAME)
.handler(importMapHandler).build());
} else {
log.warn(
"No Mvnpm jars were found in the project. Requests to the /_static/ path will always return 404 (Not Found)");
}
}
}

private LibInfo getLibInfo(CurateOutcomeBuildItem curateOutcome, String prefix, String name) {

final List<ClassPathElement> providers = QuarkusClassLoader.getElements(prefix, false);
if (!providers.isEmpty()) {
final Map<ArtifactKey, ClassPathElement> webJarKeys = new HashMap<>(providers.size());
final Map<ArtifactKey, ClassPathElement> keys = new HashMap<>(providers.size());
for (ClassPathElement provider : providers) {
if (provider.getDependencyKey() == null || !provider.isRuntime()) {
log.warn("webjars content found in " + provider.getRoot()
+ " won't be available. Please, report this issue.");
} else {
webJarKeys.put(provider.getDependencyKey(), provider);
if (provider.getDependencyKey() != null && provider.isRuntime()) {
keys.put(provider.getDependencyKey(), provider);
}
}
if (!webJarKeys.isEmpty()) {
final Map<String, String> webjarMap = new HashMap<>(webJarKeys.size());
if (!keys.isEmpty()) {
final Map<String, String> map = new HashMap<>(keys.size());
final Set<URL> jars = new HashSet<>();
for (ResolvedDependency dep : curateOutcome.getApplicationModel().getDependencies()) {
if (!dep.isRuntimeCp()) {
continue;
}
final ClassPathElement provider = webJarKeys.get(dep.getKey());

final ClassPathElement provider = keys.get(dep.getKey());
if (provider == null) {
continue;
}
provider.apply(tree -> {
final Path webjarsDir = tree.getPath(WEBJARS_PREFIX);
final Path dir = tree.getPath(prefix);
final Path nameDir;
try (Stream<Path> webjarsDirPaths = Files.list(webjarsDir)) {
nameDir = webjarsDirPaths.filter(Files::isDirectory).findFirst().get();
try (Stream<Path> dirPaths = Files.list(dir)) {
nameDir = dirPaths.filter(Files::isDirectory).findFirst().get();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
if (nameDir == null) {
log.warn("Failed to determine the name for webjars included in "
log.warn("Failed to determine the name for " + name + " included in "
+ tree.getOriginalTree().getRoots());
return null;
}
final String version = Files.isDirectory(nameDir.resolve(dep.getVersion())) ? dep.getVersion() : null;
webjarMap.put(nameDir.getFileName().toString(), version);
map.put(nameDir.getFileName().toString(), version);
try {
jars.add(dep.getResolvedPaths().getSinglePath().toUri().toURL());
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
return null;
});
}
webjarNameToVersionMap = webjarMap;

return new LibInfo(map, jars);
}
}
return null;
}

if (!webjarNameToVersionMap.isEmpty()) {
// The context path + the resources path
String rootPath = httpConfig.rootPath;
String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + "webjars/" : rootPath + "/webjars/";
feature.produce(new FeatureBuildItem(Feature.WEBJARS_LOCATOR));
routes.produce(
RouteBuildItem.builder().route("/webjars/*")
.handler(recorder.getHandler(webjarRootPath, webjarNameToVersionMap)).build());
} else {
log.warn("No WebJars were found in the project. Requests to the /webjars/ path will always return 404 (Not Found)");
private String getRootPath(HttpBuildTimeConfig httpConfig, String path) {
// The context path + the resources path
String rootPath = httpConfig.rootPath;
return (rootPath.endsWith("/")) ? rootPath + path + "/" : rootPath + "/" + path + "/";
}

static class LibInfo {
Map<String, String> nameVersionMap;
Set<URL> jars;

LibInfo(Map<String, String> nameVersionMap, Set<URL> jars) {
this.nameVersionMap = nameVersionMap;
this.jars = jars;
}

}

private static final String IMPORTMAP_ROOT = "_importmap";
private static final String IMPORTMAP_FILENAME = "generated_importmap.js";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@

import java.util.List;

import io.quarkus.builder.item.SimpleBuildItem;

public final class WebJarLibrariesBuildItem extends SimpleBuildItem {
import io.quarkus.builder.item.MultiBuildItem;

public final class WebJarLibrariesBuildItem extends MultiBuildItem {
private final String provider;
private final List<WebJarLibrary> webJarLibraries;

public WebJarLibrariesBuildItem(List<WebJarLibrary> webJarLibraries) {
public WebJarLibrariesBuildItem(String provider, List<WebJarLibrary> webJarLibraries) {
this.provider = provider;
this.webJarLibraries = webJarLibraries;
}

public List<WebJarLibrary> getWebJarLibraries() {
return webJarLibraries;
return this.webJarLibraries;
}

public String getProvider() {
return this.provider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@

public class WebJarLocatorDevModeApiProcessor {

private static final String WEBJARS_PREFIX = "META-INF/resources/webjars";
private static final String PREFIX = "META-INF/resources/";
private static final String WEBJARS_PATH = "webjars";
private static final String MVNPM_PATH = "_static";

private static final Logger log = Logger.getLogger(WebJarLocatorDevModeApiProcessor.class.getName());

@BuildStep(onlyIf = IsDevelopment.class)
Expand All @@ -39,8 +42,18 @@ public void findWebjarsAssets(
CurateOutcomeBuildItem curateOutcome,
BuildProducer<WebJarLibrariesBuildItem> webJarLibrariesProducer) {

final List<WebJarLibrary> webJarLibraries = getLibraries(httpConfig, curateOutcome, WEBJARS_PATH);
webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem("webjars", webJarLibraries));

final List<WebJarLibrary> mvnpmLibraries = getLibraries(httpConfig, curateOutcome, MVNPM_PATH);
webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem("mvnpm", mvnpmLibraries));

}

private List<WebJarLibrary> getLibraries(HttpBuildTimeConfig httpConfig,
CurateOutcomeBuildItem curateOutcome, String path) {
final List<WebJarLibrary> webJarLibraries = new ArrayList<>();
final List<ClassPathElement> providers = QuarkusClassLoader.getElements(WEBJARS_PREFIX, false);
final List<ClassPathElement> providers = QuarkusClassLoader.getElements(PREFIX + path, false);
if (!providers.isEmpty()) {
// Map of webjar artifact keys to class path elements
final Map<ArtifactKey, ClassPathElement> webJarKeys = providers.stream()
Expand All @@ -51,19 +64,21 @@ public void findWebjarsAssets(
// The root path of the application
final String rootPath = httpConfig.rootPath;
// The root path of the webjars
final String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + "webjars/" : rootPath + "/webjars/";
final String webjarRootPath = (rootPath.endsWith("/")) ? rootPath + path + "/" : rootPath + "/" + path + "/";

// For each packaged webjar dependency, create a WebJarLibrary object
curateOutcome.getApplicationModel().getDependencies().stream()
.map(dep -> createWebJarLibrary(dep, webjarRootPath, webJarKeys))
.map(dep -> createWebJarLibrary(dep, webjarRootPath, webJarKeys, path))
.filter(Objects::nonNull).forEach(webJarLibraries::add);
}
}
webJarLibrariesProducer.produce(new WebJarLibrariesBuildItem(webJarLibraries));
return webJarLibraries;
}

private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, String webjarRootPath,
Map<ArtifactKey, ClassPathElement> webJarKeys) {
private WebJarLibrary createWebJarLibrary(ResolvedDependency dep,
String webjarRootPath,
Map<ArtifactKey, ClassPathElement> webJarKeys,
String path) {
// If the dependency is not a runtime class path dependency, return null
if (!dep.isRuntimeCp()) {
return null;
Expand All @@ -74,7 +89,7 @@ private WebJarLibrary createWebJarLibrary(ResolvedDependency dep, String webjarR
}
final WebJarLibrary webJarLibrary = new WebJarLibrary(provider.getDependencyKey().getArtifactId());
provider.apply(tree -> {
final Path webjarsDir = tree.getPath(WEBJARS_PREFIX);
final Path webjarsDir = tree.getPath(PREFIX + path);
final Path nameDir;
try (Stream<Path> webjarsDirPaths = Files.list(webjarsDir)) {
nameDir = webjarsDirPaths.filter(Files::isDirectory).findFirst().orElseThrow(() -> new IOException(
Expand Down
Loading

0 comments on commit 7fd3cde

Please sign in to comment.