Part of EPIC #698.
Current behavior (probed):
- A
throw in a server view returns HTTP 200 with the component silently replaced by <!-- Module error : Error: ... -->. Fault isolation that keeps a live site up, but in development — the mode this was measured in, and the default of the Jahia Docker images — it hides the failure completely: nothing visible on the page, nothing in the watch terminal.
Correction (2026-07-24), after reading core and re-testing: the comment comes from AbstractFilter#getContentForError, which distinguishes the modes — development carries the exception message, production replaces it with a timestamp pointing at the logs. A failure landing higher in the render chain (a template rather than a fragment) propagates instead, and the page returns 500; that is what a production-mode instance did with the same content. The issue stands as written: the mode developers work in is the one that swallows the error.
- The logged stack is Java-dominated; the JS frame points at the bundled file (
hello-module/dist/server/index.js:3937). Server source maps are built (dist/server/index.js.map, on by default) but never consumed.
- A syntax error only appears in the watch terminal (esbuild code frame; the message for an unclosed JSX tag is the cryptic "Unterminated regular expression"); the page keeps serving the last good build with no indication.
Proposal — a dev flag (engine or render filter) that:
- Renders a visible error box in place of the failed component (message + source-mapped stack), instead of the HTML comment.
- Applies the existing source maps when logging GraalVM exceptions, so logs point at
src/components/Foo/default.server.tsx:12 rather than index.js:3937.
- Streams module render errors back to the
yarn dev terminal.
For reference, Astro surfaces both cases as HTTP 500 + in-browser overlay + terminal stack with exact file:line:col.
Technical plan (agent-executable)
Written for execution by a Claude (Opus/Sonnet) agent inside the Jahia Cortex harness. All paths are relative to the javascript-modules repo root.
Harness setup
- Open Cortex (
git clone https://github.com/Jahia/cortex && cd cortex && cp .env.example .env, license file into local/).
- Use the
cortex-sources skill to bring in javascript-modules (branch: main, or the js-server-extensions feature branch if these paths moved).
- Running Jahia for verification:
jahia-docker tool (Jahia ≥ 8.2). Build engine: mvn install -pl javascript-modules-engine -am -DskipTests at repo root; deploy the resulting JAR with the jahia-deploy tool.
Scope
Phase 1 (this issue): dev-mode error box + source-mapped stacks in logs. Phase 2 (terminal streaming) is a stretch goal — file a follow-up issue rather than growing this PR.
Code map
| File |
Role |
javascript-modules-engine-java/src/main/java/.../views/JSScript.java |
Executes a JS view render; exceptions bubble to Jahia core, which either swallows them into <!-- Module error : ... --> (AbstractFilter#getContentForError) or propagates them — do NOT patch core. |
javascript-modules-engine-java/src/main/java/.../jsengine/GraalVMEngine.java, GraalVMException.java |
Where GraalVM exceptions are wrapped and logged. |
vite-plugin/src/index.ts |
Server sourcemaps are already ON by default → dist/server/index.js.map ships inside every deployed module tgz. |
Implementation steps
- Source-map resolver (engine-java). New class
SourceMapService (or similar):
- Loads
dist/server/index.js.map from a module bundle's resources (the engine already reads bundle resources for GraalVM sources — reuse that access path).
- Parses the sourcemap (JSON + VLQ mappings). Prefer a small vendored VLQ decoder (~150 lines, no new OSGi dependency) over importing a library — check
mvn dependency:tree impact first; if com.atlassian.sourcemap embeds cleanly as an OSGi private package, it is acceptable.
- API:
Optional<MappedFrame> map(String bundleSymbolicName, int line, int column) returning original source path + line.
- Cache per bundle; invalidate on bundle (un)deploy — hook the same lifecycle the engine uses to recycle GraalVM contexts (
Activator bundle events).
- Stack rewriting. Utility that rewrites a
PolyglotException stack: for frames matching <js>.*\((<module>/dist/server/index\.js):(\d+)(?::(\d+))?\), substitute the mapped src/...:line. Apply it:
- in
GraalVMException message/log formatting (always — cheap and useful in prod logs too);
- in the dev error box below.
- Dev-mode error box. In
JSScript.execute (view render path only — NOT actions/extensions):
- Catch render exceptions. If Jahia runs in development mode (
SettingsBean.getInstance().getOperatingMode() — verify exact accessor; fall back to an engine config flag javascript.modules.devErrors=true in the engine's OSGi config if operating mode is not reliably readable from module scope), return an HTML fragment instead of rethrowing: module name, view registry key, exception message, source-mapped stack (<pre>), all HTML-escaped.
- Otherwise rethrow — byte-identical production behavior.
- Keep the error box unstyled-but-readable (inline styles, red border) — no external CSS.
Verification
- Unit (engine-java, JUnit): sourcemap resolver against a checked-in fixture map (generate one by building
vite-plugin/fixtures and copying dist/server/index.js.map); assert a known generated line maps to the original .tsx path/line.
- Integration (manual or cypress in
tests/):
- Add/modify a test-module view that throws (
jahia-test-module has the view harness).
- Dev mode: page shows the error box containing the ORIGINAL source path (e.g.
src/react/server/views/...tsx:NN), HTTP status unchanged (200) — assert via cy.contains or curl+grep.
- Production mode: output is exactly what it was before the change (the comment, or a propagated error, depending on where the failure lands) and NO error box.
- Logs (
docker logs): stack line shows the mapped src/....tsx:NN instead of dist/server/index.js:NNNN.
- Regression: full
tests/ cypress suite green (use jahia-cypress tool).
Acceptance criteria
Risks / notes
- HTML-escape everything injected into the error box (module code controls the message — XSS vector otherwise).
- Actions/validators/choicelists already return structured errors — leave their paths untouched.
- Column numbers from GraalVM may be 0/absent — map line-only when needed.
Part of EPIC #698.
Current behavior (probed):
throwin a server view returns HTTP 200 with the component silently replaced by<!-- Module error : Error: ... -->. Fault isolation that keeps a live site up, but in development — the mode this was measured in, and the default of the Jahia Docker images — it hides the failure completely: nothing visible on the page, nothing in the watch terminal.hello-module/dist/server/index.js:3937). Server source maps are built (dist/server/index.js.map, on by default) but never consumed.Proposal — a dev flag (engine or render filter) that:
src/components/Foo/default.server.tsx:12rather thanindex.js:3937.yarn devterminal.For reference, Astro surfaces both cases as HTTP 500 + in-browser overlay + terminal stack with exact
file:line:col.Technical plan (agent-executable)
Harness setup
git clone https://github.com/Jahia/cortex && cd cortex && cp .env.example .env, license file intolocal/).cortex-sourcesskill to bring injavascript-modules(branch:main, or the js-server-extensions feature branch if these paths moved).jahia-dockertool (Jahia ≥ 8.2). Build engine:mvn install -pl javascript-modules-engine -am -DskipTestsat repo root; deploy the resulting JAR with thejahia-deploytool.Scope
Phase 1 (this issue): dev-mode error box + source-mapped stacks in logs. Phase 2 (terminal streaming) is a stretch goal — file a follow-up issue rather than growing this PR.
Code map
javascript-modules-engine-java/src/main/java/.../views/JSScript.java<!-- Module error : ... -->(AbstractFilter#getContentForError) or propagates them — do NOT patch core.javascript-modules-engine-java/src/main/java/.../jsengine/GraalVMEngine.java,GraalVMException.javavite-plugin/src/index.tsdist/server/index.js.mapships inside every deployed module tgz.Implementation steps
SourceMapService(or similar):dist/server/index.js.mapfrom a module bundle's resources (the engine already reads bundle resources for GraalVM sources — reuse that access path).mvn dependency:treeimpact first; ifcom.atlassian.sourcemapembeds cleanly as an OSGi private package, it is acceptable.Optional<MappedFrame> map(String bundleSymbolicName, int line, int column)returning original source path + line.Activatorbundle events).PolyglotExceptionstack: for frames matching<js>.*\((<module>/dist/server/index\.js):(\d+)(?::(\d+))?\), substitute the mappedsrc/...:line. Apply it:GraalVMExceptionmessage/log formatting (always — cheap and useful in prod logs too);JSScript.execute(view render path only — NOT actions/extensions):SettingsBean.getInstance().getOperatingMode()— verify exact accessor; fall back to an engine config flagjavascript.modules.devErrors=truein the engine's OSGi config if operating mode is not reliably readable from module scope), return an HTML fragment instead of rethrowing: module name, view registry key, exception message, source-mapped stack (<pre>), all HTML-escaped.Verification
vite-plugin/fixturesand copyingdist/server/index.js.map); assert a known generated line maps to the original.tsxpath/line.tests/):jahia-test-modulehas the view harness).src/react/server/views/...tsx:NN), HTTP status unchanged (200) — assert viacy.containsor curl+grep.docker logs): stack line shows the mappedsrc/....tsx:NNinstead ofdist/server/index.js:NNNN.tests/cypress suite green (usejahia-cypresstool).Acceptance criteria
docs/1-getting-started/1-dev-environment/README.mdexplaining dev-mode errors.Risks / notes