Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4aec2b8
Add source-mapped stack traces to the Pro node renderer
justin808 Jun 11, 2026
af8b4ce
Address codex review: reject source-map entries from other generated …
justin808 Jun 11, 2026
49c44ef
Address source map review feedback
justin808 Jun 13, 2026
bfd0b2f
Bound source-map cache to live renderer requests
justin808 Jun 14, 2026
d4fcd12
Remap host callback source-map stacks
justin808 Jun 14, 2026
1489c5a
Retire failed source map registrations
justin808 Jun 14, 2026
555341d
Harden node renderer source map loading
justin808 Jun 14, 2026
2025012
Keep incremental source maps through stream completion
justin808 Jun 14, 2026
d48906c
Bound incremental close hook after stream errors
justin808 Jun 14, 2026
b74dbbd
Remap serialized host callback stacks
justin808 Jun 14, 2026
97d8248
Tighten source map review fixes
justin808 Jun 14, 2026
87777f0
Bound incremental close hook on EOF
justin808 Jun 14, 2026
fb15807
Release hung incremental response contexts
justin808 Jun 14, 2026
64a0d30
Honor sourceRoot in mapped stack frames
justin808 Jun 14, 2026
84bbad5
Isolate source map generations by VM context
justin808 Jun 14, 2026
5d87b8c
Release standard stream contexts without aborting
justin808 Jun 14, 2026
14488fb
Tighten source map review fixes
justin808 Jun 14, 2026
39b5497
Close out renderer source map CI issues
justin808 Jun 14, 2026
f83cbd8
Address renderer source map review findings
justin808 Jun 14, 2026
14d8de7
Clarify node renderer source map review feedback
justin808 Jun 14, 2026
858acfb
Align node renderer bundle hash validation
justin808 Jun 14, 2026
72c02f8
Fix source-map retain cleanup on VM build failure
justin808 Jun 14, 2026
3ac5656
Retry late external source maps safely
justin808 Jun 14, 2026
ad5cd2d
Retry partial external source maps safely
justin808 Jun 14, 2026
3cdee98
Refresh llms-full after rebase
justin808 Jun 14, 2026
555ee04
Retry partial fallback source maps safely
justin808 Jun 14, 2026
7e7fa55
Remap host-rendered source-mapped stacks
justin808 Jun 14, 2026
4f8f7a5
Count missing source-map retries per stack
justin808 Jun 14, 2026
65dc620
Avoid global remaps without a VM registration
justin808 Jun 14, 2026
503359b
Harden source-map stack remapping safety
justin808 Jun 15, 2026
09be66b
Merge remote-tracking branch 'origin/main' into jg/3893-renderer-sour…
justin808 Jun 15, 2026
b8f75d5
Retry fallback source maps after VM misses
justin808 Jun 15, 2026
f2fb549
Mark renderer string errors as plain text
justin808 Jun 15, 2026
03b74a7
Escape renderer error text responses
justin808 Jun 15, 2026
0833cd1
Release incremental context on early errors
justin808 Jun 15, 2026
4f7dcfe
Merge remote-tracking branch 'origin/main' into jg/3893-renderer-sour…
justin808 Jun 15, 2026
da9c108
Send renderer error text as plain bytes
justin808 Jun 15, 2026
8e59544
Preserve renderer error diagnostics as text
justin808 Jun 15, 2026
8797998
Document safe text error responses for CodeQL
justin808 Jun 15, 2026
738c580
Expose renderer error text content type to CodeQL
justin808 Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
#### Added

- **Machine-readable doctor output (`FORMAT=json`)**: `bin/rails react_on_rails:doctor FORMAT=json` (and `ReactOnRails::Doctor.new(format: :json)`) now emits a stable, versioned JSON report — `schema_version`, `ror_version`, overall `status`, per-check entries with stable snake_case ids (`pass`/`warn`/`fail` status, most-severe `message`, full `details`), and a `summary` of check counts — so coding agents and tooling can consume diagnosis results without parsing the human-formatted text. Stray check output is routed to stderr so stdout stays valid JSON; the default human-readable output and exit-code semantics are unchanged. Split out from the MCP-server RFC in [Issue 3870](https://github.com/shakacode/react_on_rails/issues/3870). Closes [Issue 3943](https://github.com/shakacode/react_on_rails/issues/3943). [PR 3948](https://github.com/shakacode/react_on_rails/pull/3948) by [justin808](https://github.com/justin808).
- **[Pro]** **Source-mapped stack traces in the Node renderer**: SSR errors now point at the original TypeScript/JavaScript `file:line:column` instead of bundled positions. When the server bundle carries an inline source map (or a `.map` file is available next to the uploaded bundle), the renderer captures the map text for the VM's bundle generation and lazily parses it on the first error before remapping stack frames — both for exceptions returned to Rails as renderer errors and for stacks captured inside the bundle that surface through `ReactOnRails::PrerenderError`. Bundles are also now evaluated with their real file path, so even unmapped stacks name the bundle file rather than `evalmachine.<anonymous>`. No per-request overhead: map parsing and frame remapping happen only when an error's stack is accessed, and parsed maps are cached per bundle generation. Uses Node's built-in `module.SourceMap` (no new dependencies). Part of [Issue 3893](https://github.com/shakacode/react_on_rails/issues/3893). [PR 3940](https://github.com/shakacode/react_on_rails/pull/3940) by [justin808](https://github.com/justin808).
- **Consumer-facing AI-agent guidance scaffolded into generated and installed apps**: `rails generate react_on_rails:install` (and therefore `create-react-on-rails-app`, which delegates to it) now writes a concise, app-scoped `AGENTS.md` plus thin editor-pointer files (`CLAUDE.md`, `.cursor/rules/react-on-rails.mdc`, `.github/copilot-instructions.md`) so an AI coding agent dropped into a fresh app already knows how to register a component, use the `react_component` view helper, choose `.client`/`.server` bundles, recover from the top runtime errors (sourced from `SmartError`), and run `bin/rails react_on_rails:doctor`. The errors section tracks the live `SmartError` messages, and the editor files are pointers (not copies). Emission is gated by `--agent-files`/`--no-agent-files` (default on) in both paths and never overwrites an existing file. Cross-links the eval-harness work in [Issue 3832](https://github.com/shakacode/react_on_rails/issues/3832). Closes [Issue 3868](https://github.com/shakacode/react_on_rails/issues/3868). [PR 3924](https://github.com/shakacode/react_on_rails/pull/3924) by [justin808](https://github.com/justin808).
- **First-party font optimization helper (a `next/font/local` analog)**: New `ReactOnRails::FontHelper#react_on_rails_font_face` view helper returns `<head>` markup for a committed, self-hosted `.woff2` font: a `<link rel="preload" as="font" type="font/woff2" crossorigin>`, an `@font-face` with `font-display: swap`, and an optional metric-matched fallback `@font-face` (`size-adjust` plus `ascent-override` / `descent-override` / `line-gap-override`) so the system fallback occupies the same space as the web font and the swap produces no layout shift (CLS). Self-hosting through the asset pipeline avoids any third-party font-host request. Includes a new [Font Optimization](docs/oss/building-features/fonts.md) docs page (self-hosting, preload, `font-display`, the `size-adjust` fallback technique with a worked Inter-over-Arial derivation, subsetting guidance, and a CLS note) and a runnable dummy-app example. v1 covers the `next/font/local` committed-file path and the non-streaming `react_component_hash` head-injection path. Closes [Issue 3875](https://github.com/shakacode/react_on_rails/issues/3875) (partial). Deferred sub-tasks tracked in [Issue 3921](https://github.com/shakacode/react_on_rails/issues/3921). [PR 3923](https://github.com/shakacode/react_on_rails/pull/3923) by [justin808](https://github.com/justin808).
- **Stable SmartError codes and generated error reference**: SmartError messages now include stable `ROR###` codes and canonical documentation URLs, and `docs/oss/reference/error-reference.md` is generated from the SmartError definitions with a drift check. Fixes [Issue 3894](https://github.com/shakacode/react_on_rails/issues/3894). [PR 3936](https://github.com/shakacode/react_on_rails/pull/3936) by [justin808](https://github.com/justin808).
Expand Down
9 changes: 9 additions & 0 deletions docs/oss/building-features/node-renderer/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ Use this page for breakpoints, renderer logs, and memory snapshots. For CPU flam
For renderer debugging inside this repo, use the Pro dummy app at `react_on_rails_pro/spec/dummy`.
It is a `pnpm` workspace app and already points at the local packages in this monorepo.

## Source-Mapped Stack Traces

When an SSR error occurs, the renderer remaps stack frames to the original TypeScript/JavaScript `file:line:column` — both in the error returned to Rails (surfaced as `ReactOnRails::PrerenderError` / renderer error messages) and in the renderer logs. This works automatically when the server bundle includes a source map:

- **Inline map (simplest):** build the server bundle with `devtool: 'inline-source-map'` (webpack and Rspack). The map travels inside the bundle, so nothing else needs to be uploaded. Inline maps grow the server bundle file, but the server bundle is never served to browsers, so the cost is only disk space and upload size between Rails and the renderer.
- **External map:** with `devtool: 'source-map'`, make the `.map` file available next to the uploaded bundle in the renderer's bundle cache directory. The renderer accepts a filename-only `//# sourceMappingURL=` reference next to the bundle, including trusted pre-staged symlink entries created by Pro cache staging, and also checks for `<uploaded-bundle>.js.map`. Never serve server-bundle source maps publicly.

External source map text is captured asynchronously while the VM builds so active requests keep using the map generation they started with. The map is parsed and applied lazily on the first error and cached per bundle generation, so normal requests do not pay stack-formatting cost. If no source map is found, stack traces still name the real bundle file path and line.

## Debugging the Node Renderer

### Quick start: debugging with the full stack running
Expand Down
10 changes: 10 additions & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14518,6 +14518,15 @@ Use this page for breakpoints, renderer logs, and memory snapshots. For CPU flam
For renderer debugging inside this repo, use the Pro dummy app at `react_on_rails_pro/spec/dummy`.
It is a `pnpm` workspace app and already points at the local packages in this monorepo.

## Source-Mapped Stack Traces

When an SSR error occurs, the renderer remaps stack frames to the original TypeScript/JavaScript `file:line:column` — both in the error returned to Rails (surfaced as `ReactOnRails::PrerenderError` / renderer error messages) and in the renderer logs. This works automatically when the server bundle includes a source map:

- **Inline map (simplest):** build the server bundle with `devtool: 'inline-source-map'` (webpack and Rspack). The map travels inside the bundle, so nothing else needs to be uploaded. Inline maps grow the server bundle file, but the server bundle is never served to browsers, so the cost is only disk space and upload size between Rails and the renderer.
- **External map:** with `devtool: 'source-map'`, make the `.map` file available next to the uploaded bundle in the renderer's bundle cache directory. The renderer accepts a filename-only `//# sourceMappingURL=` reference next to the bundle, including trusted pre-staged symlink entries created by Pro cache staging, and also checks for `<uploaded-bundle>.js.map`. Never serve server-bundle source maps publicly.

External source map text is captured asynchronously while the VM builds so active requests keep using the map generation they started with. The map is parsed and applied lazily on the first error and cached per bundle generation, so normal requests do not pay stack-formatting cost. If no source map is found, stack traces still name the real bundle file path and line.

## Debugging the Node Renderer

### Quick start: debugging with the full stack running
Expand Down Expand Up @@ -27988,6 +27997,7 @@ After a release, run `/update-changelog` in Claude Code to analyze commits, writ
#### Added

- **Machine-readable doctor output (`FORMAT=json`)**: `bin/rails react_on_rails:doctor FORMAT=json` (and `ReactOnRails::Doctor.new(format: :json)`) now emits a stable, versioned JSON report — `schema_version`, `ror_version`, overall `status`, per-check entries with stable snake_case ids (`pass`/`warn`/`fail` status, most-severe `message`, full `details`), and a `summary` of check counts — so coding agents and tooling can consume diagnosis results without parsing the human-formatted text. Stray check output is routed to stderr so stdout stays valid JSON; the default human-readable output and exit-code semantics are unchanged. Split out from the MCP-server RFC in [Issue 3870](https://github.com/shakacode/react_on_rails/issues/3870). Closes [Issue 3943](https://github.com/shakacode/react_on_rails/issues/3943). [PR 3948](https://github.com/shakacode/react_on_rails/pull/3948) by [justin808](https://github.com/justin808).
- **[Pro]** **Source-mapped stack traces in the Node renderer**: SSR errors now point at the original TypeScript/JavaScript `file:line:column` instead of bundled positions. When the server bundle carries an inline source map (or a `.map` file is available next to the uploaded bundle), the renderer captures the map text for the VM's bundle generation and lazily parses it on the first error before remapping stack frames — both for exceptions returned to Rails as renderer errors and for stacks captured inside the bundle that surface through `ReactOnRails::PrerenderError`. Bundles are also now evaluated with their real file path, so even unmapped stacks name the bundle file rather than `evalmachine.<anonymous>`. No per-request overhead: map parsing and frame remapping happen only when an error's stack is accessed, and parsed maps are cached per bundle generation. Uses Node's built-in `module.SourceMap` (no new dependencies). Part of [Issue 3893](https://github.com/shakacode/react_on_rails/issues/3893). [PR 3940](https://github.com/shakacode/react_on_rails/pull/3940) by [justin808](https://github.com/justin808).
- **Consumer-facing AI-agent guidance scaffolded into generated and installed apps**: `rails generate react_on_rails:install` (and therefore `create-react-on-rails-app`, which delegates to it) now writes a concise, app-scoped `AGENTS.md` plus thin editor-pointer files (`CLAUDE.md`, `.cursor/rules/react-on-rails.mdc`, `.github/copilot-instructions.md`) so an AI coding agent dropped into a fresh app already knows how to register a component, use the `react_component` view helper, choose `.client`/`.server` bundles, recover from the top runtime errors (sourced from `SmartError`), and run `bin/rails react_on_rails:doctor`. The errors section tracks the live `SmartError` messages, and the editor files are pointers (not copies). Emission is gated by `--agent-files`/`--no-agent-files` (default on) in both paths and never overwrites an existing file. Cross-links the eval-harness work in [Issue 3832](https://github.com/shakacode/react_on_rails/issues/3832). Closes [Issue 3868](https://github.com/shakacode/react_on_rails/issues/3868). [PR 3924](https://github.com/shakacode/react_on_rails/pull/3924) by [justin808](https://github.com/justin808).
- **First-party font optimization helper (a `next/font/local` analog)**: New `ReactOnRails::FontHelper#react_on_rails_font_face` view helper returns `<head>` markup for a committed, self-hosted `.woff2` font: a `<link rel="preload" as="font" type="font/woff2" crossorigin>`, an `@font-face` with `font-display: swap`, and an optional metric-matched fallback `@font-face` (`size-adjust` plus `ascent-override` / `descent-override` / `line-gap-override`) so the system fallback occupies the same space as the web font and the swap produces no layout shift (CLS). Self-hosting through the asset pipeline avoids any third-party font-host request. Includes a new [Font Optimization](docs/oss/building-features/fonts.md) docs page (self-hosting, preload, `font-display`, the `size-adjust` fallback technique with a worked Inter-over-Arial derivation, subsetting guidance, and a CLS note) and a runnable dummy-app example. v1 covers the `next/font/local` committed-file path and the non-streaming `react_component_hash` head-injection path. Closes [Issue 3875](https://github.com/shakacode/react_on_rails/issues/3875) (partial). Deferred sub-tasks tracked in [Issue 3921](https://github.com/shakacode/react_on_rails/issues/3921). [PR 3923](https://github.com/shakacode/react_on_rails/pull/3923) by [justin808](https://github.com/justin808).
- **Stable SmartError codes and generated error reference**: SmartError messages now include stable `ROR###` codes and canonical documentation URLs, and `docs/oss/reference/error-reference.md` is generated from the SmartError definitions with a drift check. Fixes [Issue 3894](https://github.com/shakacode/react_on_rails/issues/3894). [PR 3936](https://github.com/shakacode/react_on_rails/pull/3936) by [justin808](https://github.com/justin808).
Expand Down
3 changes: 3 additions & 0 deletions packages/react-on-rails-pro-node-renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "17.0.0-rc.3",
"protocolVersion": "2.0.0",
"description": "React on Rails Pro Node Renderer for server-side rendering",
"engines": {
"node": ">=18.19.0"
},
"main": "lib/ReactOnRailsProNodeRenderer.js",
"directories": {
"doc": "docs"
Expand Down
43 changes: 37 additions & 6 deletions packages/react-on-rails-pro-node-renderer/src/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import log from './log.js';
import type { TracingContext } from './tracing.js';
import type { RenderResult } from '../worker/vm.js';
import fileExistsAsync from './fileExistsAsync.js';
import { remapStackTrace } from '../worker/vmSourceMapSupport.js';
Comment thread
justin808 marked this conversation as resolved.

export const TRUNCATION_FILLER = '\n... TRUNCATED ...\n';

Expand Down Expand Up @@ -59,7 +60,12 @@ export function smartTrim(value: unknown, maxLength = getConfig().maxDebugSnippe
}

export interface ResponseResult {
headers: { 'Cache-Control'?: string };
headers: {
'Cache-Control'?: string;
'Content-Type'?: string;
'X-Content-Type-Options'?: string;
[key: string]: string | undefined;
};
status: number;
data?: unknown;
stream?: Readable;
Expand All @@ -85,10 +91,19 @@ export type RequestInfo = { renderingRequest: string } | { label: string; conten
* @param request Either a rendering request (auto-labeled) or a { label, content } pair
* @param error The error that was thrown (typed as `unknown` to minimize casts in `catch`)
* @param context Optional context to include in the error message
* @param stackRemapper Defaults to scanning registered source-map bundles. VM request paths pass a
* registration-scoped remapper to avoid rewriting unrelated bundle paths.
*/
export function formatExceptionMessage(request: RequestInfo, error: unknown, context?: string) {
export function formatExceptionMessage(
request: RequestInfo,
error: unknown,
context?: string,
stackRemapper = remapStackTrace,
Comment thread
justin808 marked this conversation as resolved.
Comment thread
justin808 marked this conversation as resolved.
) {
const label = 'renderingRequest' in request ? 'JS code for rendering request was:' : request.label;
const content = 'renderingRequest' in request ? request.renderingRequest : request.content;
const rawStack = (error as Error).stack;
const stack = stackRemapper(rawStack) ?? rawStack;

return `${context ? `\nContext:\n${context}\n` : ''}
${label}
Expand All @@ -98,7 +113,7 @@ EXCEPTION MESSAGE:
${(error as Error).message || error}

STACK:
${(error as Error).stack}`;
${stack}`;
}

// https://github.com/fastify/fastify-multipart?tab=readme-ov-file#usage
Expand Down Expand Up @@ -204,14 +219,30 @@ export const delay = (milliseconds: number) =>
setTimeout(resolve, milliseconds);
});

// Keep aligned with ReactOnRailsPro::RollingDeploy::SAFE_HASH_PATTERN.
const BUNDLE_TIMESTAMP_PATH_COMPONENT_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/;

function bundleTimestampPathComponent(bundleTimestamp: string | number) {
const pathComponent = String(bundleTimestamp);
if (!BUNDLE_TIMESTAMP_PATH_COMPONENT_PATTERN.test(pathComponent)) {
throw new Error(
`Invalid bundle timestamp path component: ${pathComponent}. ` +
'Expected only letters, digits, dots, underscores, and hyphens.',
);
}

return pathComponent;
}

export function getBundleDirectory(bundleTimestamp: string | number) {
const { serverBundleCachePath } = getConfig();
return path.join(serverBundleCachePath, `${bundleTimestamp}`);
return path.resolve(serverBundleCachePath, bundleTimestampPathComponent(bundleTimestamp));
}

export function getRequestBundleFilePath(bundleTimestamp: string | number) {
const bundleDirectory = getBundleDirectory(bundleTimestamp);
return path.join(bundleDirectory, `${bundleTimestamp}.js`);
const pathComponent = bundleTimestampPathComponent(bundleTimestamp);
const bundleDirectory = getBundleDirectory(pathComponent);
return path.join(bundleDirectory, `${pathComponent}.js`);
}

export function getAssetPath(bundleTimestamp: string | number, filename: string) {
Expand Down
Loading
Loading