This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new d2d55e2ad750 CAMEL-25199: camel-jbang - read Java routes with the Java
DSL parser only, retire the regex readers (#27147)
d2d55e2ad750 is described below
commit d2d55e2ad7502f497f8d4a19968fac957e3e3478
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 09:12:48 2026 +0200
CAMEL-25199: camel-jbang - read Java routes with the Java DSL parser only,
retire the regex readers (#27147)
- the parser tells a call of configure() from its declaration, so an
anonymous
RouteBuilder passed to camel.configure().addRoutesBuilder(...) is read
- the project overview reads Java with the parser only; the regex reader is
removed (over the example projects the parser now reads every route it
did)
- the consumer check of camel validate reads the endpoints Java routes
consume
with the parser: constants, String.format and the endpoint DSL are
resolved,
a route template or an input known only at runtime still keeps it quiet
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/java/in/JavaChainParser.java | 5 +-
.../org/apache/camel/java/in/LwJavaParserTest.java | 23 +++
design/java-dsl-parser.adoc | 11 ++
.../core/commands/ai/EndpointConsumerChecks.java | 49 ++++--
.../dsl/jbang/core/commands/ai/ProjectRoutes.java | 184 +--------------------
.../jbang/core/commands/ai/SourceValidator.java | 2 +-
.../ai/SourceValidatorEndpointConsumersTest.java | 51 ++++++
7 files changed, 125 insertions(+), 200 deletions(-)
diff --git
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
index c90ba3fbc0f9..9950092f7b11 100644
---
a/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
+++
b/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java
@@ -251,7 +251,10 @@ final class JavaChainParser {
} else if (t.is("}")) {
depth--;
pos++;
- } else if ((t.isIdent("configure") || t.isIdent("configuration"))
&& at(1).is("(") && at(2).is(")")) {
+ } else if ((t.isIdent("configure") || t.isIdent("configuration"))
&& at(1).is("(") && at(2).is(")")
+ && (at(3).is("{") || at(3).isIdent("throws"))) {
+ // the declaration of configure(), not a call such as
camel.configure().addRoutesBuilder(...), whose
+ // anonymous RouteBuilder has a configure() of its own
int p = pos + 3;
// throws clause
while (tokens.get(p).kind() != Kind.EOF &&
!tokens.get(p).is("{") && !tokens.get(p).is(";")) {
diff --git
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
index 4939cb8ccea0..a9a2b6ff5cc5 100644
---
a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
+++
b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java
@@ -267,6 +267,29 @@ class LwJavaParserTest {
assertThat(cb.getConfiguration()).isEqualTo("myConfig");
}
+ @Test
+ void anAnonymousRouteBuilderPassedToACall() {
+ // CAMEL-25199: camel.configure() is a call, the configure() of the
anonymous RouteBuilder the declaration
+ JavaParseResult result = new LwJavaParser().parse("""
+ public final class Application {
+ public static void main(String[] args) throws Exception {
+ try (Main camel = new Main()) {
+ camel.configure().addRoutesBuilder(new
RouteBuilder() {
+ @Override
+ public void configure() throws Exception {
+
from("timer:foo?repeatCount=1").to("direct:aggregator");
+ from("direct:aggregator").to("log:out");
+ }
+ });
+ camel.run();
+ }
+ }
+ }
+ """);
+ assertThat(result.routes().getRoutes()).extracting(r ->
r.getInput().getUri())
+ .containsExactly("timer:foo?repeatCount=1",
"direct:aggregator");
+ }
+
@Test
void aRouteKeptInALocalVariable() {
// a route built in steps: RouteDefinition route = from(...);
route.to(...)
diff --git a/design/java-dsl-parser.adoc b/design/java-dsl-parser.adoc
index 500bcf7f9fa9..1a9c187194b9 100644
--- a/design/java-dsl-parser.adoc
+++ b/design/java-dsl-parser.adoc
@@ -91,6 +91,17 @@ the most specific; candidates are tried in a stable order.
source files, and the catalog's component header constants
(`KafkaConstants.KEY`) and enum values
(`HazelcastOperation.PUT`).
+=== Where it is used
+
+camel-jbang reads Java routes with the parser only; the pattern matching on
string literals it had before is gone
+(CAMEL-25199):
+
+* the project overview (`camel overview`, the `camel_project_overview` MCP
tool, the TUI Architecture and Topology
+ views);
+* the consumer check of `camel validate`: which `direct:` and `seda:`
endpoints the Java routes of an application
+ consume, with constants, formats and the endpoint DSL resolved;
+* the jump links between routes in the TUI Source tab.
+
== Security
The parser reads sources nobody vetted: a cloned example opened in the TUI,
documentation, a project an AI agent
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
index ed9c848ff39f..23a8de47fab6 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
@@ -32,7 +32,11 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.dsl.yaml.validator.EndpointConsumers;
+import org.apache.camel.java.in.JavaParseResult;
+import org.apache.camel.java.in.LwJavaParser;
+import org.apache.camel.model.RouteDefinition;
/**
* A {@code direct:} or {@code seda:} endpoint a YAML route sends to, and no
route of the application consumes
@@ -41,13 +45,8 @@ import org.apache.camel.dsl.yaml.validator.EndpointConsumers;
*/
public final class EndpointConsumerChecks {
- /**
- * A Java DSL route input: from( not called on something else, so not
Instant.from( or List.from(; its endpoint is
- * read only when a string literal is the whole argument, not the start of
from("direct:" + NAME).
- */
- private static final Pattern JAVA_FROM =
Pattern.compile("(?<![.\\w])from\\s*\\(\\s*(\"([^\"]*)\"\\s*(?=[),]))?");
- /** Route inputs the scan cannot read: a route template's from( and fromF(
with a format. */
- private static final Pattern JAVA_UNREADABLE =
Pattern.compile("\\b(routeTemplate|fromF)\\s*\\(");
+ /** A Java DSL route input: from( not called on something else, so not
Instant.from( or List.from(. */
+ private static final Pattern JAVA_FROM =
Pattern.compile("(?<![.\\w])from\\s*\\(");
/** The build files of a project whose routes are spread over
src/main/java and src/main/resources. */
private static final List<String> BUILD_FILES = List.of("pom.xml",
"build.gradle", "build.gradle.kts");
private static final Pattern XML_FROM =
Pattern.compile("<from\\s[^>]*?\\buri\\s*=\\s*[\"']([^\"']*)[\"']");
@@ -62,7 +61,15 @@ public final class EndpointConsumerChecks {
* @return the messages, one per endpoint no route consumes
*/
public static List<String> validateYamlConsumers(String content, Path
directory, String excludeFile) {
- return EndpointConsumers.check(content, consumed(directory,
excludeFile));
+ return validateYamlConsumers(content, directory, excludeFile, null);
+ }
+
+ /**
+ * @param catalog for the endpoint DSL of Java routes, may be null
+ */
+ public static List<String> validateYamlConsumers(
+ String content, Path directory, String excludeFile, CamelCatalog
catalog) {
+ return EndpointConsumers.check(content, consumed(directory,
excludeFile, catalog));
}
/**
@@ -71,7 +78,7 @@ public final class EndpointConsumerChecks {
* Gradle project, more files than the scan looks at, or a route input the
scan cannot read (a Java {@code from(}
* with no literal, a placeholder, a route template), which could be any
endpoint.
*/
- static Set<String> consumed(Path directory, String excludeFile) {
+ static Set<String> consumed(Path directory, String excludeFile,
CamelCatalog catalog) {
if (directory == null || !Files.isDirectory(directory)) {
return null;
}
@@ -91,7 +98,7 @@ public final class EndpointConsumerChecks {
if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
found = EndpointConsumers.consumed(Files.readString(p));
} else if (lower.endsWith(".java")) {
- found = javaConsumed(Files.readString(p));
+ found = javaConsumed(Files.readString(p), catalog);
} else {
found = xmlConsumed(Files.readString(p));
}
@@ -177,15 +184,25 @@ public final class EndpointConsumerChecks {
&& (lower.endsWith(".yaml") || lower.endsWith(".yml") ||
lower.endsWith(".java") || lower.endsWith(".xml"));
}
- static Set<String> javaConsumed(String src) {
- if (JAVA_UNREADABLE.matcher(src).find()) {
+ /**
+ * The endpoints the routes of a Java source consume, read into the model
by the Java DSL parser (CAMEL-25199), so
+ * constants, String.format and the endpoint DSL are resolved; null when
they cannot be known: a route template,
+ * whose input is a parameter, an input known only at runtime, or a from(
the parser could not read.
+ */
+ static Set<String> javaConsumed(String src, CamelCatalog catalog) {
+ JavaParseResult result = JavaRouteReader.parse(src, catalog, null);
+ if (!result.routeTemplates().getRouteTemplates().isEmpty()) {
+ return null;
+ }
+ List<RouteDefinition> routes = result.routes().getRoutes();
+ if (routes.isEmpty() && JAVA_FROM.matcher(src).find()) {
+ // a from( the parser did not read as a route: what it consumes is
not known
return null;
}
Set<String> answer = new HashSet<>();
- Matcher m = JAVA_FROM.matcher(src);
- while (m.find()) {
- // from(someConstant) or from(direct("x")): the endpoint is not in
the source as a literal
- if (m.group(2) == null || !add(m.group(2), answer)) {
+ for (RouteDefinition r : routes) {
+ String uri = r.getInput() != null ? r.getInput().getUri() : null;
+ if (uri == null || uri.contains(LwJavaParser.UNRESOLVED_PREFIX) ||
!add(uri, answer)) {
return null;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
index f71bb9b25539..5bd573654a33 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ProjectRoutes.java
@@ -78,19 +78,6 @@ public final class ProjectRoutes {
/** The id given to an onException or error handler outside a route,
followed by where it is written. */
public static final String ERROR_HANDLER_PREFIX = "error-handler@";
- private static final Pattern JAVA_FROM =
Pattern.compile("(?<![.\\w])from\\s*\\(\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
- private static final Pattern JAVA_CALL = Pattern.compile(
-
"\\.(to|toD|wireTap|enrich|pollEnrich|poll|routeId|routeDescription|routeNote|routeGroup|kamelet|recipientList|routingSlip"
- +
"|dynamicRouter)\\s*\\(\\s*(\"((?:[^\"\\\\]|\\\\.)*)\")?");
- private static final Pattern JAVA_STEP =
Pattern.compile("\\.(\\w+)\\s*\\(");
- private static final Pattern JAVA_ON_EXCEPTION =
Pattern.compile("(?<![.\\w])onException\\s*\\(");
- private static final Pattern JAVA_DEAD_LETTER = Pattern.compile(
- "deadLetterChannel\\s*\\(\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
- private static final Pattern JAVA_TO =
Pattern.compile("\\.to\\s*\\(\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
- /** Java DSL calls that do not change what a route does: naming and
describing it. */
- private static final Set<String> JAVA_NAMING
- = Set.of("routeId", "routeDescription", "routeNote", "routeGroup",
"id", "description", "note");
-
private ProjectRoutes() {
}
@@ -184,9 +171,8 @@ public final class ProjectRoutes {
return new XmlReader(file, content, catalog).read();
}
if (lower.endsWith(".java")) {
- // the Java DSL parser gives the model of the routes; the
patterns only when it finds none
- List<Route> routes = JavaRouteReader.read(file, content,
catalog, constants);
- return !routes.isEmpty() ? routes : readJava(file, content,
catalog);
+ // read into the model by the Java DSL parser, without
compiling the source
+ return JavaRouteReader.read(file, content, catalog, constants);
}
} catch (RuntimeException e) {
// an overview skips what it cannot read; the validator reports
what is wrong with the file
@@ -1095,172 +1081,6 @@ public final class ProjectRoutes {
return line;
}
- // ---- Java ----
-
- /**
- * The routes of a Java RouteBuilder, one per {@code from("...")}: every
string literal passed to an endpoint method
- * until the next from belongs to it.
- */
- private static List<Route> readJava(String file, String content,
CamelCatalog catalog) {
- String code = stripComments(content);
- List<Route> routes = new ArrayList<>();
- Matcher from = JAVA_FROM.matcher(code);
- List<int[]> starts = new ArrayList<>();
- List<String> uris = new ArrayList<>();
- while (from.find()) {
- starts.add(new int[] { from.start(), from.end() });
- uris.add(unescape(from.group(1)));
- }
- for (int i = 0; i < starts.size(); i++) {
- int end = i + 1 < starts.size() ? starts.get(i + 1)[0] :
code.length();
- String body = code.substring(starts.get(i)[1], end);
- // a statement ends the route: stop at the first semicolon outside
a string
- int stop = statementEnd(body);
- body = body.substring(0, stop);
- Endpoint fromEp = endpoint(uris.get(i), null, false, catalog);
- List<Endpoint> consumes = new ArrayList<>();
- List<Endpoint> produces = new ArrayList<>();
- add(consumes, fromEp);
- String id = null;
- String description = null;
- String note = null;
- String group = null;
- Matcher m = JAVA_CALL.matcher(body);
- while (m.find()) {
- String method = m.group(1);
- String literal = m.group(3) != null ? unescape(m.group(3)) :
null;
- switch (method) {
- case "routeId" -> id = literal;
- case "routeDescription" -> description = literal;
- case "routeNote" -> note = literal;
- case "routeGroup" -> group = literal;
- case "kamelet" ->
- add(produces, literal != null ? endpoint("kamelet:" +
literal, null, false, catalog) : null);
- case "poll", "pollEnrich" -> add(consumes,
- literal != null ? endpoint(literal, null, true,
catalog) : dynamicEndpoint(method));
- case "recipientList", "routingSlip", "dynamicRouter" ->
produces.add(dynamicEndpoint(method));
- default -> add(produces, literal != null
- ? endpoint(literal, null, !"to".equals(method),
catalog) : dynamicEndpoint(method));
- }
- }
- int steps = 0;
- Matcher s = JAVA_STEP.matcher(body);
- while (s.find()) {
- steps++;
- }
- routes.add(new Route(
- id, "route", description, group, file, lineAt(content,
starts.get(i)[0]),
- "java", true, fromEp, consumes, produces, steps, -1, 0,
null, null,
- javaLogOnly(body, produces), note));
- }
- javaErrorHandlers(file, content, code, catalog, routes);
- return routes;
- }
-
- /** onException statements and dead letter channels outside a route, as
error handler routes. */
- private static void javaErrorHandlers(
- String file, String content, String code, CamelCatalog catalog,
List<Route> routes) {
- List<Endpoint> produces = new ArrayList<>();
- int first = -1;
- Matcher on = JAVA_ON_EXCEPTION.matcher(code);
- while (on.find()) {
- String statement = code.substring(on.start(), on.start() +
statementEnd(code.substring(on.start())));
- Matcher to = JAVA_TO.matcher(statement);
- while (to.find()) {
- add(produces, endpoint(unescape(to.group(1)), null, false,
catalog));
- first = first < 0 ? on.start() : first;
- }
- }
- Matcher dlc = JAVA_DEAD_LETTER.matcher(code);
- while (dlc.find()) {
- add(produces, endpoint(unescape(dlc.group(1)), null, false,
catalog));
- first = first < 0 ? dlc.start() : Math.min(first, dlc.start());
- }
- if (!produces.isEmpty()) {
- int line = lineAt(content, first);
- routes.add(new Route(
- ERROR_HANDLER_PREFIX + file + ":" + line, "errorHandler",
null, null, file, line, "java", true, null,
- List.of(),
produces.stream().map(Endpoint::asOnError).toList(), 0, -1, 0, null, null,
false, null));
- }
- }
-
- /** Whether a Java route only logs: its calls are log, to a log endpoint,
or naming the route. */
- private static boolean javaLogOnly(String body, List<Endpoint> produces) {
- boolean logs = false;
- Matcher m = JAVA_STEP.matcher(body);
- while (m.find()) {
- String method = m.group(1);
- if ("log".equals(method) || "to".equals(method)) {
- logs = true;
- } else if (!JAVA_NAMING.contains(method)) {
- return false;
- }
- }
- return logs && produces.stream().allMatch(e ->
"log".equals(e.scheme()));
- }
-
- /** Comments are replaced by spaces so offsets in the code still match the
file. */
- private static String stripComments(String content) {
- StringBuilder sb = new StringBuilder(content.length());
- int i = 0;
- boolean inString = false;
- while (i < content.length()) {
- char c = content.charAt(i);
- if (inString) {
- sb.append(c);
- if (c == '\\' && i + 1 < content.length()) {
- sb.append(content.charAt(++i));
- } else if (c == '"') {
- inString = false;
- }
- i++;
- } else if (c == '"') {
- inString = true;
- sb.append(c);
- i++;
- } else if (c == '/' && i + 1 < content.length() &&
content.charAt(i + 1) == '/') {
- while (i < content.length() && content.charAt(i) != '\n') {
- sb.append(' ');
- i++;
- }
- } else if (c == '/' && i + 1 < content.length() &&
content.charAt(i + 1) == '*') {
- int end = content.indexOf("*/", i + 2);
- end = end < 0 ? content.length() : end + 2;
- while (i < end) {
- sb.append(content.charAt(i) == '\n' ? '\n' : ' ');
- i++;
- }
- } else {
- sb.append(c);
- i++;
- }
- }
- return sb.toString();
- }
-
- private static int statementEnd(String body) {
- boolean inString = false;
- for (int i = 0; i < body.length(); i++) {
- char c = body.charAt(i);
- if (inString) {
- if (c == '\\') {
- i++;
- } else if (c == '"') {
- inString = false;
- }
- } else if (c == '"') {
- inString = true;
- } else if (c == ';') {
- return i;
- }
- }
- return body.length();
- }
-
- private static String unescape(String literal) {
- return literal.replace("\\\"", "\"").replace("\\\\", "\\");
- }
-
/**
* An endpoint URI for showing or sending to a model: the query is
sanitized so a password or token in it is masked.
*/
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 459be17c7fe7..f0102539d63d 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -127,7 +127,7 @@ public final class SourceValidator {
msgs.addAll(GroovyImportChecks.validateYamlGroovyImports(content, null,
declarations.javaClasses()));
if (checkConsumers) {
// a direct: or seda: endpoint no route of the application
consumes (CAMEL-24955)
-
msgs.addAll(EndpointConsumerChecks.validateYamlConsumers(content, directory,
fileName));
+
msgs.addAll(EndpointConsumerChecks.validateYamlConsumers(content, directory,
fileName, catalog));
}
}
return msgs;
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
index c752979d5f63..703c3c777e20 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
@@ -144,6 +144,57 @@ class SourceValidatorEndpointConsumersTest {
}
}
+ @Test
+ void theJavaRoutesAreReadByTheParser() throws Exception {
+ // CAMEL-25199: a constant, a format and the endpoint DSL are
resolved, so the check knows what they consume
+ for (String configure : List.of(
+ "from(OTHER).log(\"found\");",
+ "fromF(\"direct:%s\", \"other\").log(\"found\");",
+ "from(direct(\"other\")).log(\"found\");")) {
+ Files.writeString(dir.resolve("Other.java"), """
+ import
org.apache.camel.builder.endpoint.EndpointRouteBuilder;
+
+ public class Other extends EndpointRouteBuilder {
+ static final String OTHER = "direct:other";
+
+ @Override
+ public void configure() {
+ %s
+ }
+ }
+ """.formatted(configure));
+ assertThat(SourceValidator.validate("r.camel.yaml", ROUTE,
CATALOG, null, dir)).as(configure)
+ .singleElement().asString().startsWith("route tick: sends
to direct:lookup, and no route consumes it");
+ }
+ // the same with the endpoint DSL naming the endpoint the YAML route
sends to: consumed
+ Files.writeString(dir.resolve("Other.java"), """
+ import org.apache.camel.builder.endpoint.EndpointRouteBuilder;
+
+ public class Other extends EndpointRouteBuilder {
+ @Override
+ public void configure() {
+ from(direct("lookup")).log("found");
+ }
+ }
+ """);
+ assertThat(SourceValidator.validate("r.camel.yaml", ROUTE, CATALOG,
null, dir)).isEmpty();
+ }
+
+ @Test
+ void aJavaRouteInputKnownOnlyAtRuntimeKeepsTheCheckQuiet() throws
Exception {
+ Files.writeString(dir.resolve("Lookup.java"), """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Lookup extends RouteBuilder {
+ @Override
+ public void configure() {
+ from(inputFromConfig()).log("found");
+ }
+ }
+ """);
+ assertThat(SourceValidator.validate("r.camel.yaml", ROUTE, CATALOG,
null, dir)).isEmpty();
+ }
+
@Test
void aRouteFileInASubdirectoryConsumesIt() throws Exception {
Files.createDirectories(dir.resolve("routes"));