Skip to content

Dev error surfacing: visible errors, source-mapped stacks #700

Description

@romain-pm

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:

  1. Renders a visible error box in place of the failed component (message + source-mapped stack), instead of the HTML comment.
  2. 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.
  3. 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

  1. Open Cortex (git clone https://github.com/Jahia/cortex && cd cortex && cp .env.example .env, license file into local/).
  2. Use the cortex-sources skill to bring in javascript-modules (branch: main, or the js-server-extensions feature branch if these paths moved).
  3. 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

  1. 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).
  2. 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.
  3. 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.
  4. Keep the error box unstyled-but-readable (inline styles, red border) — no external CSS.

Verification

  1. 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.
  2. 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.
  3. Regression: full tests/ cypress suite green (use jahia-cypress tool).

Acceptance criteria

  • A throwing view in dev mode renders a visible, escaped error box with a source-mapped stack; production output is unchanged.
  • Engine logs show source-mapped frames for module exceptions (dev AND prod).
  • No new runtime dependency leaks into module bundles; engine JAR size increase < 100 KB.
  • Unit test for the resolver + at least one e2e asserting the dev box and the prod comment.
  • Docs: one section in docs/1-getting-started/1-dev-environment/README.md explaining dev-mode errors.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions