diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index 25c5149188eb4..b0c007df14313 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -158,7 +158,7 @@ public enum Feature { VAULT, VERTX, VERTX_GRAPHQL, - WEBJARS_LOCATOR; + WEB_DEPENDENCY_LOCATOR; public String getName() { return toString().toLowerCase().replace('_', '-'); diff --git a/docs/src/main/asciidoc/http-reference.adoc b/docs/src/main/asciidoc/http-reference.adoc index 736b0649dc77d..dab5add984c87 100644 --- a/docs/src/main/asciidoc/http-reference.adoc +++ b/docs/src/main/asciidoc/http-reference.adoc @@ -12,7 +12,6 @@ include::_attributes.adoc[] :sectnumlevels: 4 :topics: http,web,webjars,mvnpm,vertx,servlet,undertow :extensions: io.quarkus:quarkus-vertx-http -:web-locator-ga: quarkus-web-dependency-locator This document clarifies different HTTP functionalities available in Quarkus. @@ -35,93 +34,7 @@ location to function correctly. === From web dependencies like webjars or mvnpm -==== WebJars -If you are using https://www.webjars.org[WebJars], like the following JQuery one: - -[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - org.webjars - jquery - 3.1.1 - ----- - -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle ----- -implementation("org.webjars:jquery:3.1.1") ----- - -and rather write `/webjars/jquery/jquery.min.js` instead of `/webjars/jquery/3.1.1/jquery.min.js` -in your HTML files, you can add the `{web-locator-ga}` extension to your project. -To use it, add the following to your project's dependencies: - -[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - io.quarkus - {web-locator-ga} - ----- - -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle ----- -implementation("io.quarkus:{web-locator-ga}") ----- - -==== Mvnpm - -If you are using https://mvnpm.org[mvnpm], like the following Lit one: - -[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] -.pom.xml ----- - - org.mvnpm - lit - 3.1.2 - ----- - -[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] -.build.gradle ----- -implementation("org.mvnpm:lit:3.1.2") ----- - -you can use the `{web-locator-ga}` as described above to reference the resource without the version, however with mvnpm you can -also use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap[importmaps]. - -The importmap is generated by the `{web-locator-ga}` extension, and available at `/_importmap/generated_importmap.js`. -This mean adding the following to your `index.html` will allow you to import web libraries by name: - -[source,html] ----- - - - - - - My app - - <1> - - - - - ----- -<1> Use the generated importmap -<2> Import web libraries -<3> Import your own files, this can be done by adding `quarkus.web-dependency-locator.import-mappings.app/ = /app/` to the config. Any key-value pair can be added. - +Look at the xref:web-dependency-locator.adoc[Web dependency locator] guide for details on how to use https://www.webjars.org[WebJars], https://mvnpm.org[mvnpm] and https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap[importmaps] === From a local directory diff --git a/docs/src/main/asciidoc/images/web-dependency-locator-screenshot01.png b/docs/src/main/asciidoc/images/web-dependency-locator-screenshot01.png new file mode 100644 index 0000000000000..28289f0df08f0 Binary files /dev/null and b/docs/src/main/asciidoc/images/web-dependency-locator-screenshot01.png differ diff --git a/docs/src/main/asciidoc/images/web-dependency-locator-screenshot02.png b/docs/src/main/asciidoc/images/web-dependency-locator-screenshot02.png new file mode 100644 index 0000000000000..8e6b723b1e28b Binary files /dev/null and b/docs/src/main/asciidoc/images/web-dependency-locator-screenshot02.png differ diff --git a/docs/src/main/asciidoc/images/web-dependency-locator-screenshot03.png b/docs/src/main/asciidoc/images/web-dependency-locator-screenshot03.png new file mode 100644 index 0000000000000..3633bc9a4c6e1 Binary files /dev/null and b/docs/src/main/asciidoc/images/web-dependency-locator-screenshot03.png differ diff --git a/docs/src/main/asciidoc/web-dependency-locator.adoc b/docs/src/main/asciidoc/web-dependency-locator.adoc new file mode 100644 index 0000000000000..cebdc12b92db2 --- /dev/null +++ b/docs/src/main/asciidoc/web-dependency-locator.adoc @@ -0,0 +1,144 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Web dependency locator +include::_attributes.adoc[] +:categories: web +:summary: Learn more about how to use the web dependency locator +:numbered: +:sectnums: +:sectnumlevels: 4 +:topics: http,web,webjars,mvnpm,vertx,servlet,undertow +:extensions: io.quarkus:quarkus-web-dependency-locator +:web-locator-ga: quarkus-web-dependency-locator + +This document shows how static resources can be served from web dependency jars like https://www.webjars.org[WebJars] and https://mvnpm.org[mvnpm]. + +== Using the `{web-locator-ga}` extension + +[source,xml,subs="attributes+",role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + io.quarkus + {web-locator-ga} + +---- + +[source,gradle,subs="attributes+",role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("io.quarkus:{web-locator-ga}") +---- + +=== WebJars +If you are using https://www.webjars.org[WebJars], like the following JQuery one: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + org.webjars + jquery + 3.1.1 + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("org.webjars:jquery:3.1.1") +---- + +you can reference the files in the jar from your HTML, example `/webjars/jquery/3.1.1/jquery.min.js`. + +The above is available by default, however, adding the `{web-locator-ga}` extension allows you to reference the files without having to include the version in the path, example `/webjars/jquery/jquery.min.js`. + +=== Mvnpm + +If you are using https://mvnpm.org[mvnpm], like the following Lit one: + +[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml +---- + + org.mvnpm + lit + 3.1.2 + +---- + +[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle +---- +implementation("org.mvnpm:lit:3.1.2") +---- + +you can reference the files in the jar from your HTML, example `/_static/lit/3.1.2/index.js`. + +The above is available by default, however, adding the `{web-locator-ga}` extension allows you to reference the files without having to include the version in the path, example `/_static/lit/index.js`. + +==== ImportMaps + +Mvnpm jars also allows you to use an https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap[importmap], that will +allow you to just use module imports, example `import { LitElement, html, css} from 'lit';`. + +The importmap is generated by the `{web-locator-ga}` extension, and available at `/_importmap/generated_importmap.js`. +This means adding the following to your `index.html` will allow you to import web libraries by name: + +[source,html] +---- + + + + + + My app + + <1> + + + + + +---- +<1> Use the generated importmap +<2> Import web libraries +<3> Import your own files, this can be done by adding `quarkus.web-dependency-locator.import-mappings.app/ = /app/` to the config. Any key-value pair can be added. + +===== Automatic imports + +You can also automate the imports above. To do this, move your web assests from `src/main/resources/META-INF/resources` to `src/main/web` +and now replace the above scripts and imports with `{#bundle /}`: + +[source,html] +---- + + + + + + My app + + {#bundle /} <1> + + + + +---- +<1> This will be replaced at build time with the importmap script, and also include any CSS and JavaScript discovered in the `/app` directory. + +This allows you to add libraries, js and css without having to change your HTML. Hot-reload is also supported. + +=== Dev UI + +When adding the `{web-locator-ga}` extension , you can see the files being served, and the generated importmap, in the Dev UI: + +image:web-dependency-locator-screenshot01.png[alt=Card in Dev UI] +image:web-dependency-locator-screenshot02.png[alt=Files] +image:web-dependency-locator-screenshot03.png[alt=Importmap] \ No newline at end of file diff --git a/docs/src/main/asciidoc/web.adoc b/docs/src/main/asciidoc/web.adoc index 932f265e1d194..dc3cf6f74aa62 100644 --- a/docs/src/main/asciidoc/web.adoc +++ b/docs/src/main/asciidoc/web.adoc @@ -28,7 +28,7 @@ You can find more information in the xref:http-reference#serving-static-resource However, if you want to insert scripts, styles, and libraries in your web pages, you have 3 options: a. Consume libraries from public CDNs such as cdnjs, unpkg, jsDelivr and more, or copy them to your `META-INF/resources` directory. -b. Use runtime web dependencies such as mvnpm.org or webjars, when added to your pom.xml or build.gradle they can be directly xref:http-reference#mvnpm[accessed from your web pages]. +b. Use runtime web dependencies such as https://mvnpm.org[mvnpm] or https://www.webjars.org[WebJars], when added to your pom.xml or build.gradle they can be directly xref:web-dependency-locator.adoc[accessed from your web pages]. c. Package your scripts (js, ts), styles (css, scss), and web dependencies together using a bundler (see xref:#bundling[below]). NOTE: *We recommend using a bundler for production* as it offers better control, consistency, security, and performance. The good news is that Quarkus makes it really easy and fast with the https://docs.quarkiverse.io/quarkus-web-bundler/dev/[Quarkus Web Bundler extension]. diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java index a4248e5e853b5..5889caba572a5 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyStandaloneBuildStep.java @@ -38,7 +38,6 @@ import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.AuthenticationRedirectException; import io.quarkus.vertx.core.deployment.CoreVertxBuildItem; -import io.quarkus.vertx.http.deployment.DefaultRouteBuildItem; import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; @@ -89,7 +88,6 @@ public void staticInit(ResteasyStandaloneRecorder recorder, public void boot(ShutdownContextBuildItem shutdown, ResteasyStandaloneRecorder recorder, BuildProducer feature, - BuildProducer defaultRoutes, BuildProducer routes, BuildProducer filterBuildItemBuildProducer, CoreVertxBuildItem vertx, diff --git a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java index 6592ea84704d1..377a4233a1203 100644 --- a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorConfig.java @@ -23,4 +23,15 @@ public class WebDependencyLocatorConfig { @ConfigItem public Map importMappings; + /** + * The directory in the resources which serves as root for the web assets + */ + @ConfigItem(defaultValue = "web") + public String webRoot; + + /** + * The directory in the resources which serves as root for the app assets + */ + @ConfigItem(defaultValue = "app") + public String appRoot; } diff --git a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java index 2841ccae3bfc1..15263d63e91ba 100644 --- a/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java +++ b/extensions/web-dependency-locator/deployment/src/main/java/io/quarkus/webdependency/locator/deployment/WebDependencyLocatorProcessor.java @@ -1,16 +1,20 @@ package io.quarkus.webdependency.locator.deployment; import java.io.IOException; +import java.io.StringWriter; import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import java.util.stream.Stream; import org.jboss.logging.Logger; @@ -24,7 +28,9 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; import io.quarkus.maven.dependency.ArtifactKey; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.vertx.http.deployment.RouteBuildItem; @@ -34,21 +40,135 @@ import io.vertx.ext.web.RoutingContext; public class WebDependencyLocatorProcessor { + private static final Logger log = Logger.getLogger(WebDependencyLocatorProcessor.class.getName()); - private static final String WEBJARS_PREFIX = "META-INF/resources/webjars"; - private static final String WEBJARS_NAME = "webjars"; + @BuildStep + public void findRelevantFiles(BuildProducer feature, + BuildProducer hotDeploymentWatchedProducer, + WebDependencyLocatorConfig config, + OutputTargetBuildItem outputTarget) throws IOException { - private static final String MVNPM_PREFIX = "META-INF/resources/_static"; - private static final String MVNPM_NAME = "mvnpm"; + Path web = outputTarget.getOutputDirectory().getParent() + .resolve(SRC) + .resolve(MAIN) + .resolve(RESOURCES) + .resolve(config.webRoot); - private static final Logger log = Logger.getLogger(WebDependencyLocatorProcessor.class.getName()); + if (Files.exists(web)) { + hotDeploymentWatchedProducer.produce(new HotDeploymentWatchedFileBuildItem(config.webRoot + SLASH + STAR)); + // Find all css and js (under /app) + Path app = web + .resolve(config.appRoot); + + List cssFiles = new ArrayList<>(); + List jsFiles = new ArrayList<>(); + + if (Files.exists(app)) { + hotDeploymentWatchedProducer + .produce(new HotDeploymentWatchedFileBuildItem(config.webRoot + SLASH + config.appRoot + SLASH + STAR)); + try (Stream appstream = Files.walk(app)) { + appstream.forEach(path -> { + if (Files.isRegularFile(path) && path.toString().endsWith(DOT_CSS)) { + cssFiles.add(web.relativize(path)); + } else if (Files.isRegularFile(path) && path.toString().endsWith(DOT_JS)) { + jsFiles.add(web.relativize(path)); + } + }); + } + } + + try (Stream webstream = Files.walk(web)) { + + final Path resourcesDirectory = outputTarget.getOutputDirectory() + .resolve(CLASSES) + .resolve(META_INF) + .resolve(RESOURCES); + Files.createDirectories(resourcesDirectory); + + webstream.forEach(path -> { + if (Files.isRegularFile(path)) { + try { + copyResource(resourcesDirectory, web, path, cssFiles, jsFiles, path.toString().endsWith(DOT_HTML)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else if (Files.isRegularFile(path)) { + + } + }); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + feature.produce(new FeatureBuildItem(Feature.WEB_DEPENDENCY_LOCATOR)); + } + + private void copyResource(Path resourcesDirectory, Path webRoot, Path path, List cssFiles, List jsFiles, + boolean filter) + throws IOException { + try { + + Path relativizePath = webRoot.relativize(path); + + byte[] toBeCopied; + if (filter) { + StringJoiner modifiedContent = new StringJoiner(System.lineSeparator()); + + Files.lines(path).forEach(line -> { + String modifiedLine = processLine(line, cssFiles, jsFiles); + modifiedContent.add(modifiedLine); + }); + + String result = modifiedContent.toString(); + toBeCopied = result.getBytes(); + } else { + toBeCopied = Files.readAllBytes(path); + } + + final Path resourceFile = resourcesDirectory.resolve(relativizePath); + Files.createDirectories(resourceFile.getParent()); + Files.write(resourceFile, toBeCopied, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private static String processLine(String line, List cssFiles, List jsFiles) { + if (line.contains(HASH_IMPORTMAP)) { + line = TAB2 + IMPORTMAP_REPLACEMENT; + } + + // Let's also support web-bundler style + if (line.contains(HASH_BUNDLE)) { + try (StringWriter sw = new StringWriter()) { + if (!cssFiles.isEmpty()) { + for (Path css : cssFiles) { + sw.write(TAB2 + "" + NL); + } + } + sw.write(TAB2 + IMPORTMAP_REPLACEMENT); + if (!jsFiles.isEmpty()) { + sw.write(NL); + sw.write(TAB2 + ""); + } + line = sw.toString(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + return line; + } @BuildStep @Record(ExecutionTime.RUNTIME_INIT) public void findWebDependenciesAndCreateHandler( WebDependencyLocatorConfig config, HttpBuildTimeConfig httpConfig, - BuildProducer feature, BuildProducer routes, BuildProducer im, CurateOutcomeBuildItem curateOutcome, @@ -58,26 +178,23 @@ public void findWebDependenciesAndCreateHandler( 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 handler = recorder.getHandler(getRootPath(httpConfig, "webjars"), - webjarsLibInfo.nameVersionMap); - routes.produce(RouteBuildItem.builder().route("/webjars/*").handler(handler).build()); + routes.produce(createRouteBuildItem(recorder, httpConfig, WEBJARS_PATH, webjarsLibInfo.nameVersionMap)); } - } 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 handler = recorder.getHandler(getRootPath(httpConfig, "_static"), - mvnpmNameLibInfo.nameVersionMap); - routes.produce(RouteBuildItem.builder().route("/_static/*").handler(handler).build()); + routes.produce(createRouteBuildItem(recorder, httpConfig, MVNPM_PATH, mvnpmNameLibInfo.nameVersionMap)); } // Also create a importmap endpoint Aggregator aggregator = new Aggregator(mvnpmNameLibInfo.jars); + Map importMappings = config.importMappings; + if (!importMappings.containsKey(config.appRoot + SLASH)) { + // Add default for app/ + importMappings.put(config.appRoot + SLASH, SLASH + config.appRoot + SLASH); + } if (!config.importMappings.isEmpty()) { aggregator.addMappings(config.importMappings); } @@ -88,13 +205,21 @@ public void findWebDependenciesAndCreateHandler( Handler importMapHandler = recorder.getImportMapHandler(path, importMap); routes.produce( - RouteBuildItem.builder().route("/" + IMPORTMAP_ROOT + "/" + IMPORTMAP_FILENAME) + RouteBuildItem.builder().route(SLASH + IMPORTMAP_ROOT + SLASH + 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)"); } + } else { + log.warn( + "No WebJars or mvnpm jars were found in the project. Requests to the /webjars/ and/or /_static/ path will always return 404 (Not Found)"); } + + } + + private RouteBuildItem createRouteBuildItem(WebDependencyLocatorRecorder recorder, HttpBuildTimeConfig httpConfig, + String path, Map nameVersionMap) { + Handler handler = recorder.getHandler(getRootPath(httpConfig, path), + nameVersionMap); + return RouteBuildItem.builder().route(SLASH + path + SLASH + STAR).handler(handler).build(); } private LibInfo getLibInfo(CurateOutcomeBuildItem curateOutcome, String prefix, String name) { @@ -166,6 +291,36 @@ static class LibInfo { } + private static final String WEBJARS_PREFIX = "META-INF/resources/webjars"; + private static final String WEBJARS_NAME = "webjars"; + private static final String WEBJARS_PATH = "webjars"; + + private static final String MVNPM_PREFIX = "META-INF/resources/_static"; + private static final String MVNPM_NAME = "mvnpm"; + private static final String MVNPM_PATH = "_static"; + private static final String IMPORTMAP_ROOT = "_importmap"; private static final String IMPORTMAP_FILENAME = "generated_importmap.js"; + + private static final String HASH_BUNDLE = "#bundle"; + private static final String HASH_IMPORTMAP = "#importmap"; + private static final String IMPORTMAP_REPLACEMENT = ""; + private static final String TAB = "\t"; + private static final String TAB2 = TAB + TAB; + + private static final String CLASSES = "classes"; + private static final String META_INF = "META-INF"; + private static final String RESOURCES = "resources"; + + private static final String SRC = "src"; + private static final String MAIN = "main"; + + private static final String SLASH = "/"; + private static final String STAR = "*"; + + private static final String DOT_HTML = ".html"; + private static final String DOT_CSS = ".css"; + private static final String DOT_JS = ".js"; + + private static final String NL = "\n"; } diff --git a/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java b/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java index 2e27661e4480a..1620a9c3a67fc 100644 --- a/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java +++ b/extensions/web-dependency-locator/runtime/src/main/java/io/quarkus/webdependency/locator/runtime/WebDependencyLocatorRecorder.java @@ -35,8 +35,7 @@ public Handler getHandler(String webDependenciesRootUrl, + webDependencyNameToVersionMap.get(webdep) + rest.substring(rest.indexOf('/'))); } } else { - // this is not a web dependency that we know about - event.fail(404); + event.next(); } } else { // should not happen if route is set up correctly