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 /