From 5ef4696a1bdea797469b72d20eead16dd5839119 Mon Sep 17 00:00:00 2001 From: Romain Gauthier Date: Fri, 24 Jul 2026 22:10:22 +0200 Subject: [PATCH 1/2] feat(engine): report failing views with source-mapped stack traces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a view threw, Jahia replaced the fragment with an HTML comment and logged an exception whose JavaScript frames pointed inside the module's bundle (`dist/server/index.js:3937`). Both halves of that are unhelpful: the failure is invisible on the page unless you read the source, and the stack does not say which file you wrote is at fault. The engine now reads the source map the build already emits next to a module's server bundle, and rewrites JS frames back to module sources — `src/components/Foo/default.server.tsx:12`. Maps are parsed on first use and dropped when a module is unregistered, so a redeploy picks up new sources. Frames without a map (the library, the engine itself) are left untouched. In development mode a failing view also renders a visible, HTML-escaped error box in place of its fragment, carrying the message and the mapped stack. In production the exception propagates exactly as before; only the new log line is added. Verified on two Jahia 8.2 instances, one in each mode, against the test module's crashing view: the box appears in development with the frame mapped to TestCrashingView.tsx:11 (the failing line), production output is unchanged, and both modes log the mapped trace. Nine unit tests cover the VLQ decoding, segment lookup and frame rewriting. Closes #700 --- .chachalog/Rt6yWm4L.md | 6 + .../1-dev-environment/README.md | 6 + .../engine/jsengine/GraalVMEngine.java | 12 +- .../modules/engine/jsengine/SourceMaps.java | 270 ++++++++++++++++++ .../modules/engine/views/JSScript.java | 16 ++ .../modules/engine/views/ViewRenderError.java | 85 ++++++ .../engine/jsengine/SourceMapsTest.java | 110 +++++++ 7 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 .chachalog/Rt6yWm4L.md create mode 100644 javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/SourceMaps.java create mode 100644 javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/views/ViewRenderError.java create mode 100644 javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/jsengine/SourceMapsTest.java diff --git a/.chachalog/Rt6yWm4L.md b/.chachalog/Rt6yWm4L.md new file mode 100644 index 00000000..cf4686d7 --- /dev/null +++ b/.chachalog/Rt6yWm4L.md @@ -0,0 +1,6 @@ +--- +# Allowed version bumps: patch, minor, major +javascript-modules: minor +--- + +A view that throws now reports itself. In development mode the failing fragment is replaced by a visible error box holding the message and the stack trace, instead of an HTML comment that only the page source revealed. Stack traces — in the box and in the server log, in every mode — have their positions mapped back to the module's own sources through the source map shipped next to the server bundle, so frames read `src/components/Foo/default.server.tsx:12` rather than `dist/server/index.js:3937`. Production rendering is unchanged. (#700) diff --git a/docs/1-getting-started/1-dev-environment/README.md b/docs/1-getting-started/1-dev-environment/README.md index f9d45a45..24b36983 100644 --- a/docs/1-getting-started/1-dev-environment/README.md +++ b/docs/1-getting-started/1-dev-environment/README.md @@ -104,4 +104,10 @@ Now that your _template set_ (that's how we call a module that provides page tem Congratulations! You have successfully set up your development environment and created a new project in Jahia. In the next sections, we'll start building the project. +## When a view fails + +The Docker instance above runs Jahia in development mode. When one of your views throws, the fragment it should have produced is replaced on the page by a red box carrying the error message and its stack trace, with positions mapped back to your own sources (`src/components/…/default.server.tsx:12`) rather than to the bundle the build produced. The rest of the page still renders, so a broken component does not take the whole page down. + +The same error is written to the server log, which you can follow with `docker compose logs -f jahia`. In production mode the box is not rendered — a live site keeps Jahia's default behaviour — but the log keeps the mapped stack trace. + Next: [Making a Hero Section](making-a-hero-section) diff --git a/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/GraalVMEngine.java b/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/GraalVMEngine.java index 2baabd4f..4622e049 100644 --- a/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/GraalVMEngine.java +++ b/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/GraalVMEngine.java @@ -73,6 +73,7 @@ public class GraalVMEngine { private final ThreadLocal> currentContext = ThreadLocal.withInitial(Stack::new); private final Map initScripts = Collections.synchronizedMap(new LinkedHashMap<>()); + private final SourceMaps sourceMaps = new SourceMaps(); private final AtomicInteger version = new AtomicInteger(0); private BundleContext bundleContext; @@ -83,8 +84,9 @@ public BundleContext getBundleContext() { public void enableJavascriptModule(Bundle bundle) { try { - initScripts.put(bundle, - getGraalSource(bundle, bundle.getHeaders().get(BUNDLE_HEADER_JAVASCRIPT_INIT_SCRIPT))); + String initScript = bundle.getHeaders().get(BUNDLE_HEADER_JAVASCRIPT_INIT_SCRIPT); + initScripts.put(bundle, getGraalSource(bundle, initScript)); + sourceMaps.register(bundle, initScript); version.incrementAndGet(); logger.info("Registered bundle {} in GraalVM engine", bundle.getSymbolicName()); } catch (IOException ioe) { @@ -94,11 +96,17 @@ public void enableJavascriptModule(Bundle bundle) { public void disableJavascriptModule(Bundle bundle) { if (initScripts.remove(bundle) != null) { + sourceMaps.unregister(bundle); version.incrementAndGet(); logger.info("Unregistered bundle {} from GraalVM engine", bundle.getSymbolicName()); } } + /** Maps positions in module bundles back to the sources they were built from. */ + public SourceMaps getSourceMaps() { + return sourceMaps; + } + @Activate public void activate(BundleContext bundleContext, Map props) { logger.info("Registering GraalVMEngine"); diff --git a/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/SourceMaps.java b/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/SourceMaps.java new file mode 100644 index 00000000..b88851c1 --- /dev/null +++ b/javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/jsengine/SourceMaps.java @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2002-2023 Jahia Solutions Group SA. All rights reserved. + * + * 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 org.jahia.modules.javascript.modules.engine.jsengine; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.osgi.framework.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Maps positions in a module's bundled server script back to the sources it was built from, so that + * stack traces name the file a developer wrote rather than a line number in {@code index.js}. + * + *

Modules ship a standard source map next to their server + * bundle (the Vite plugin emits it by default). Maps are parsed on first use and dropped when the + * module is unregistered. + */ +public class SourceMaps { + + private static final Logger logger = LoggerFactory.getLogger(SourceMaps.class); + + /** + * A frame as GraalVM reports it, e.g. {@code my-module/dist/server/index.js:1234} or + * {@code …index.js:1234:5}. The script name is the Graal source name, built by + * {@link GraalVMEngine} as {@code /