fix(ext-view): clear cross-engine error, propagate globals across JteExtension builder chain

- BaseViewExtension: wrap the ViewRuntimeBridge provider collision with a message
  naming the real constraint (one view engine per FlashApp) instead of the generic
  "duplicate provider" error.
- BaseViewExtension/JteExtension: carry registered globals across JteExtension's
  immutable settings builders (templateRoot/serveStatics/staticPrefix/withStaticCors/
  staticCors). addGlobal() called before any of those used to be silently dropped,
  since each builder method returned a fresh instance with an empty globals list.
- Correct flash-ext-view-jte docs (README, architecture.md, model-and-globals.md):
  globals merge flat with one typed @param per key, not under a global.* namespace
  like Thymeleaf — the docs previously claimed the same reserved namespace for both
  engines, which doesn't match JteRuntime's actual merge behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Zakaria El Orche
2026-09-09 13:31:21 +00:00
co-authored by Claude Sonnet 5
parent 9de2800a83
commit 4feabc45d5
7 changed files with 128 additions and 11 deletions
@@ -29,31 +29,32 @@ public final class JteExtension extends BaseViewExtension<JteTarget> {
this.settings = builder.build();
}
private JteExtension(JteSettings settings) {
private JteExtension(JteSettings settings, List<GlobalValue> seedGlobals) {
super(seedGlobals);
this.settings = settings;
}
public JteExtension templateRoot(String templateRoot) {
return new JteExtension(settings.toBuilder().templateRoot(templateRoot).build());
return new JteExtension(settings.toBuilder().templateRoot(templateRoot).build(), globals());
}
public JteExtension serveStatics(boolean serveStatics) {
return new JteExtension(settings.toBuilder().serveStatics(serveStatics).build());
return new JteExtension(settings.toBuilder().serveStatics(serveStatics).build(), globals());
}
public JteExtension staticPrefix(String staticPrefix) {
return new JteExtension(settings.toBuilder().staticPrefix(staticPrefix).build());
return new JteExtension(settings.toBuilder().staticPrefix(staticPrefix).build(), globals());
}
public JteExtension withStaticCors() {
return new JteExtension(settings.toBuilder().enableStaticCors(true).build());
return new JteExtension(settings.toBuilder().enableStaticCors(true).build(), globals());
}
public JteExtension staticCors(Consumer<JteSettings.Builder> corsConfig) {
JteSettings.Builder builder = settings.toBuilder();
builder.enableStaticCors(true);
corsConfig.accept(builder);
return new JteExtension(builder.build());
return new JteExtension(builder.build(), globals());
}
@Override
@@ -1,6 +1,11 @@
package dev.relism.flash.ext.view.jte;
import dev.relism.flash.ext.view.core.RenderedView;
import dev.relism.flash.ext.view.core.ViewModel;
import dev.relism.flash.ext.view.core.ViewRuntimeBridge;
import dev.relism.flash.ext.view.jte.model.HomePage;
import dev.relism.flash.extension.FlashContext;
import dev.relism.flash.http.ContentType;
import dev.relism.flash.models.Request;
import dev.relism.flash.models.RequestHandler;
import dev.relism.flash.models.Response;
@@ -89,6 +94,39 @@ class JteExtensionTest {
assertEquals("https://cdn.example.com", settings.staticCorsAllowOrigin());
}
@Test
void addGlobal_survives_subsequent_settings_builder_calls() throws Exception {
// addGlobal() before a chained settings method (templateRoot/serveStatics/...) used to
// be silently dropped: those methods return a *new* JteExtension instance, and the
// globals list lived on the instance, not the settings being carried forward.
JteExtension ext = new JteExtension(cfg -> cfg.developmentMode(true))
.addGlobal("appName", req -> "FlashLab")
.templateRoot("templates")
.serveStatics(false);
TestRegistrar app = new TestRegistrar();
FlashContext ctx = new FlashContext();
ext.configure(app, ctx);
ctx.complete();
@SuppressWarnings("unchecked")
ViewRuntimeBridge<JteTarget> runtime = (ViewRuntimeBridge<JteTarget>) ctx.require(ViewRuntimeBridge.class);
JteHandler handler = new JteHandler() {
@Override
public ViewModel render(Request req) {
return ViewModel.empty()
.with("page", new HomePage("Flash + jte", "elorc"))
.with("build", "dev");
}
};
JteTarget target = new JteTarget("pages/home.jte", ContentType.TEXT_HTML);
RenderedView out = runtime.render(handler, target, request("/", null, null, null),
new Response(200, ContentType.JSON));
assertTrue(out.body().contains("FlashLab"), "global registered before templateRoot() must survive");
}
@Test
void routes_register_static_wildcard_when_enabled() {
JteExtension ext = new JteExtension(cfg -> cfg.staticPrefix("/assets"));