Skip to content

feat: better MDX error handling in CLI and browser#157

Merged
rsbh merged 6 commits into
mainfrom
feat/mdx-error-handling
Jul 9, 2026
Merged

feat: better MDX error handling in CLI and browser#157
rsbh merged 6 commits into
mainfrom
feat/mdx-error-handling

Conversation

@rsbh

@rsbh rsbh commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Content errors (unknown components, unclosed/mismatched tags) previously surfaced as a bare 500 JSON response in the browser and a raw stack trace from chronicle build. This PR makes both name the exact file, position, and component.

  • Compile-time validation (remark-validate-mdx): every JSX element in content is checked against standard HTML/SVG elements (html-tag-names/svg-tag-names), registered Chronicle components, and components imported/defined in the MDX file itself. Custom elements (<my-widget>) and member expressions (<Tabs.Tab>) are allowed. Unknown names fail compilation with file, line, and column.
  • Dev error page: styled page (light/dark) showing the error message, file:line:col, and a highlighted code frame. Hooked into the Nitro error handler (where content compile errors land, since the eager frontmatter glob compiles all content at startup) and the SSR catch. Polls every second and auto-reloads once the content is fixed — no server restart or manual refresh needed.
  • chronicle build: fails with exit 1 and a formatted report (relative path, line:column, message, code frame) for MDX errors; unrelated errors are rethrown unchanged.
  • Client navigation: load failures show a "Failed to render page" state with the error detail instead of the misleading 404 empty state.

Screenshots of behavior

Screenshot 2026-07-09 at 11 03 08 AM Screenshot 2026-07-09 at 11 03 35 AM

Testing

  • 19 new unit tests (remark-validate-mdx, mdx-error parser/code frame); full suite 199 pass
  • Verified live against examples/basic: unknown component and unclosed tag both render the dev error page with correct file/position; healthy pages unaffected; fixing the file recovers without restart; chronicle build exits 1 with formatted output

🤖 Generated with Claude Code

rsbh and others added 5 commits July 9, 2026 11:05
Fail compilation with file, line, and column when content uses a
component that is not a standard HTML/SVG element, a registered
Chronicle component, or imported/defined in the MDX file. Tag lists
come from html-tag-names/svg-tag-names; custom elements and member
expressions on registered components are allowed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the bare 500/JSON response with a styled dev-only error page
naming the failing file, position, and a highlighted code frame.
Handles both compile failures (unclosed tags, unknown components) via
the Nitro error handler — where they land because the eager frontmatter
glob compiles all content at startup — and runtime MDX errors via the
SSR catch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Catch build failures, print a formatted report (relative path,
line:column, message, code frame) for MDX-related errors, and exit
non-zero; unrelated errors are rethrown unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Track the failure message in page context and show a dedicated
'Failed to render page' state with the error detail, instead of the
misleading 'Page not found' empty state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The error page polls its own URL every second and reloads once the
page compiles again, or when the error changes (e.g. the next broken
file). No manual refresh or server restart needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chronicle Ready Ready Preview, Comment Jul 9, 2026 6:04am

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rsbh, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 48 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c8615d2f-9ce9-44f6-b8dd-9f78eed28390

📥 Commits

Reviewing files that changed from the base of the PR and between b902b89 and 2756c43.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • packages/chronicle/package.json
📝 Walkthrough

Walkthrough

Adds MDX component/tag validation via a new remark plugin, MDX error parsing utilities with code-frame generation, CLI build error reporting, dev-server and SSR MDX error responses with auto-reload, and client-side page context/UI changes to render non-404 errors via a new RenderError component.

Changes

MDX validation and error reporting

Layer / File(s) Summary
Component name and known-tag registries
src/lib/mdx-component-names.ts, src/components/mdx/index.tsx, package.json
Adds MDX_COMPONENT_NAMES and KNOWN_TAGS (from html-tag-names/svg-tag-names), plus sync comments and new dependencies (vfile, html-tag-names, svg-tag-names).
remarkValidateMdx plugin and tests
src/lib/remark-validate-mdx.ts, src/lib/remark-validate-mdx.test.ts, src/server/vite-config.ts
New remark plugin validates MDX JSX component/tag names against known tags, allowed components, and locally imported names, failing on unknowns; registered in Vite MDX config; covered by tests.
MDX error parsing and code frame utilities
src/lib/mdx-error.ts, src/lib/mdx-error.test.ts
Adds MdxErrorInfo/CodeFrame types, parseMdxError to detect and clean MDX-related errors, and buildCodeFrame to build surrounding-source code frames, with unit tests.
CLI build error reporting
src/cli/commands/build.ts, src/cli/utils/mdx-error-report.ts
Wraps build execution in try/catch; on failure, printMdxBuildError formats MDX errors with a code frame and the CLI exits with code 1 if handled, otherwise rethrows.
Dev server and SSR MDX error responses
src/server/dev-error-page.ts, src/server/entry-server.tsx, src/server/error.ts
Adds renderMdxErrorResponse producing a 500 HTML error page with code frame and auto-reload polling script; wired into SSR fetch error handling and the default error handler for dev mode.
Client-side render error state and UI
src/lib/page-context.tsx, src/pages/DocsPage.tsx, src/pages/RenderError.tsx, src/pages/NotFound.module.css
Adds errorMessage to page context state, set/cleared during load and route changes; DocsPage renders new RenderError component for non-404 errors, styled via new .errorDetail class.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DocsPage
  participant PageProvider as page-context
  participant EntryServer
  participant DevErrorPage as dev-error-page
  participant RenderError

  PageProvider->>PageProvider: loadDocsPage() fails, catch(err)
  PageProvider->>PageProvider: derive errorStatus, set errorMessage
  DocsPage->>PageProvider: usePageContext() reads errorStatus/errorMessage
  DocsPage->>RenderError: render RenderError(message) if errorStatus != NOT_FOUND
  EntryServer->>DevErrorPage: renderMdxErrorResponse(err) in DEV
  DevErrorPage-->>EntryServer: HTML error Response or null
Loading

Possibly related PRs

  • raystack/chronicle#24: Builds on the same buildCommand build pipeline in build.ts that the MDX error reporting try/catch wraps.
  • raystack/chronicle#35: Modifies the same page-context.tsx/DocsPage.tsx error state and routing logic extended here with errorMessage/RenderError.
  • raystack/chronicle#46: Modifies the same mdxOptions.remarkPlugins array in vite-config.ts extended here with remarkValidateMdx.

Suggested reviewers: rohilsurana, rohanchkrabrty

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title matches the main change: improved MDX error handling across CLI and browser flows.
Description check ✅ Passed The description accurately describes the PR’s MDX validation and error-handling changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mdx-error-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rsbh rsbh requested review from rohanchkrabrty and rohilsurana and removed request for rohilsurana July 9, 2026 05:57
@base-ui/react@1.6.0 renamed the OTPFieldPreview export to OTPField,
but @raystack/apsara (rc.7 and 1.0.1) still imports OTPFieldPreview
under a ^1.4.1 range. Fresh installs (Docker CI runner stage, new dev
machines) resolved 1.6.0 and failed the build with MISSING_EXPORT.
A direct >=1.4.1 <1.6.0 dependency makes resolvers dedupe onto 1.5.0.
Remove once apsara ships a base-ui 1.6-compatible release.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/chronicle/src/lib/remark-validate-mdx.test.ts (1)

19-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding test coverage for local declarations beyond imports.

collectLocalNames handles ExportNamedDeclaration, top-level VariableDeclaration, FunctionDeclaration, and ClassDeclaration, but only ImportDeclaration is exercised via importNode. Adding a helper for local declarations (e.g., export const Foo = ... or function Foo() {}) would verify those paths.

🧪 Suggested test helpers
+function varDeclNode(name: string) {
+  return {
+    type: 'mdxjsEsm',
+    value: `const ${name} = () => null`,
+    data: {
+      estree: {
+        type: 'Program',
+        body: [
+          {
+            type: 'VariableDeclaration',
+            declarations: [{ id: { name } }],
+          },
+        ],
+      },
+    },
+  } as unknown as Root['children'][number];
+}
+
+function exportNamedNode(name: string) {
+  return {
+    type: 'mdxjsEsm',
+    value: `export const ${name} = () => null`,
+    data: {
+      estree: {
+        type: 'Program',
+        body: [
+          {
+            type: 'ExportNamedDeclaration',
+            declaration: {
+              type: 'VariableDeclaration',
+              declarations: [{ id: { name } }],
+            },
+          },
+        ],
+      },
+    },
+  } as unknown as Root['children'][number];
+}

Then add tests:

+  test('allows locally declared variables', () => {
+    expect(() => run([varDeclNode('Widget'), jsxNode('Widget')])).not.toThrow();
+  });
+
+  test('allows exported named declarations', () => {
+    expect(() => run([exportNamedNode('Gadget'), jsxNode('Gadget')])).not.toThrow();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/chronicle/src/lib/remark-validate-mdx.test.ts` around lines 19 - 41,
The test suite currently covers only import-based local names via importNode,
but collectLocalNames also handles ExportNamedDeclaration, top-level
VariableDeclaration, FunctionDeclaration, and ClassDeclaration. Add a small
helper in remark-validate-mdx.test.ts for building mdxjsEsm nodes with local
declarations, then add cases that exercise those declaration paths through
remarkValidateMdx() and run(), so the helper verifies collectLocalNames across
imports, exports, variables, functions, and classes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/chronicle/src/lib/remark-validate-mdx.test.ts`:
- Around line 19-41: The test suite currently covers only import-based local
names via importNode, but collectLocalNames also handles ExportNamedDeclaration,
top-level VariableDeclaration, FunctionDeclaration, and ClassDeclaration. Add a
small helper in remark-validate-mdx.test.ts for building mdxjsEsm nodes with
local declarations, then add cases that exercise those declaration paths through
remarkValidateMdx() and run(), so the helper verifies collectLocalNames across
imports, exports, variables, functions, and classes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba54c8ad-2463-4c17-9b17-c97484ef65f6

📥 Commits

Reviewing files that changed from the base of the PR and between f1c3a05 and b902b89.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • packages/chronicle/package.json
  • packages/chronicle/src/cli/commands/build.ts
  • packages/chronicle/src/cli/utils/mdx-error-report.ts
  • packages/chronicle/src/components/mdx/index.tsx
  • packages/chronicle/src/lib/mdx-component-names.ts
  • packages/chronicle/src/lib/mdx-error.test.ts
  • packages/chronicle/src/lib/mdx-error.ts
  • packages/chronicle/src/lib/page-context.tsx
  • packages/chronicle/src/lib/remark-validate-mdx.test.ts
  • packages/chronicle/src/lib/remark-validate-mdx.ts
  • packages/chronicle/src/pages/DocsPage.tsx
  • packages/chronicle/src/pages/NotFound.module.css
  • packages/chronicle/src/pages/RenderError.tsx
  • packages/chronicle/src/server/dev-error-page.ts
  • packages/chronicle/src/server/entry-server.tsx
  • packages/chronicle/src/server/error.ts
  • packages/chronicle/src/server/vite-config.ts

@rsbh rsbh merged commit 5de3a02 into main Jul 9, 2026
9 checks passed
@rsbh rsbh deleted the feat/mdx-error-handling branch July 9, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants