Repository navigation
feat: replace AJV with vscode-json-languageservice - #474
Conversation
Preparing to use git worktrees for isolated feature development. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
…eMirror interactions
…om package.json and pnpm-lock.yaml
… validation tests
…e.json and pnpm-lock.yaml
📝 WalkthroughWalkthroughThis PR refactors JSON schema validation from ajv to vscode-json-languageservice, consolidates React rendering utilities into a single module with extended functionality, updates consumers to use the new rendering API, enhances diagnostic tooltip styling, and expands test coverage including Vega-Lite schema integration. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
packages/codemirror/__mocks__/@sqlrooms/utils.ts (1)
1-2: Update the file header comment for accuracy.Line 2 says only the React rendering function is mocked, but this file also mocks
getMonospaceFont.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/codemirror/__mocks__/`@sqlrooms/utils.ts around lines 1 - 2, Update the file header comment to accurately reflect what is mocked: mention that both the React rendering function and getMonospaceFont are mocked (not just the React rendering function). Reference the mock file packages/codemirror/__mocks__/@sqlrooms/utils.ts and the exported symbol getMonospaceFont (and the React render function) so the header clearly states both exports are mocked and the rest are no-ops.packages/codemirror/src/utils/validate-json-schema.ts (2)
92-106: UseDiagnosticSeverityenum values instead of magic numbers.The
convertSeverityfunction uses literal numbers (1, 2, 3, 4) instead of the importedDiagnosticSeverityenum, reducing readability and maintainability.♻️ Proposed fix to use enum values
function convertSeverity( vsSeverity: DiagnosticSeverity | undefined, ): Diagnostic['severity'] { switch (vsSeverity) { - case 1: + case DiagnosticSeverity.Error: return 'error'; - case 2: + case DiagnosticSeverity.Warning: return 'warning'; - case 3: + case DiagnosticSeverity.Information: return 'info'; - case 4: + case DiagnosticSeverity.Hint: return 'hint'; default: return 'warning'; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 92 - 106, The convertSeverity function currently compares vsSeverity to magic numbers (1-4); replace those numeric literals with the corresponding DiagnosticSeverity enum members (DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint) so the switch checks enum values and returns the same string mapping ('error', 'warning', 'info', 'hint'), and keep the default return as 'warning'; update the switch cases in convertSeverity to use the DiagnosticSeverity enum identifiers to improve readability and maintainability.
57-62: React root leak inrenderMessagecallbacks.Each diagnostic's
renderMessagecallback creates a new React root viarenderComponentToDomElement. These roots are never unmounted when CodeMirror removes the diagnostic tooltips. With many diagnostics (e.g., a malformed JSON file), this could accumulate orphaned roots.This is consistent with the pattern used elsewhere in this PR, but worth noting for awareness.
Also applies to: 76-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 57 - 62, renderMessage currently creates a new React root for each diagnostic by calling renderComponentToDomElement(DiagnosticTooltip, ...) and never unmounts it, leaking roots; change renderMessage to create the React root into the tooltip DOM element and return a cleanup/unmount callback (or reuse a single root) so the React tree is unmounted when CodeMirror disposes the tooltip; update both occurrences (renderMessage at lines shown and the similar block around 76-80) to call the appropriate unmount method (or keep a shared root) for the DiagnosticTooltip instead of leaving roots orphaned.packages/codemirror/src/utils/create-json-schema-validator.ts (1)
11-22: Consider adding a timeout for external schema fetches.The
schemaRequestServiceusesfetchwithout a timeout orAbortSignal. If an external schema URL is slow or unresponsive, validation could hang indefinitely.⏱️ Proposed fix to add fetch timeout
schemaRequestService: async (uri: string) => { try { - const response = await fetch(uri); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout + try { + const response = await fetch(uri, { signal: controller.signal }); + clearTimeout(timeoutId);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/codemirror/src/utils/create-json-schema-validator.ts` around lines 11 - 22, The schemaRequestService implementation uses fetch without a timeout, so slow/unresponsive schema URIs can hang validation; modify the schemaRequestService async function to create an AbortController, start a timer (e.g., timeoutMs constant or parameter) that calls controller.abort() after the timeout, pass controller.signal to fetch(uri, { signal: controller.signal }), clear the timer on success, and convert AbortError to a clear error message before throwing; update references to schemaRequestService in create-json-schema-validator to ensure the timeout is applied for all external schema fetches.packages/codemirror/__tests__/vega-schema-integration.test.ts (1)
7-13: Network-dependent test may be flaky in CI.The test fetches the Vega-Lite schema from an external URL on every run. This introduces network dependency that could cause intermittent test failures in restricted environments or during GitHub outages.
Consider caching the schema locally or mocking the fetch in CI, while keeping the real fetch for local development verification.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/codemirror/__tests__/vega-schema-integration.test.ts` around lines 7 - 13, The beforeAll block currently fetches the Vega-Lite schema from the network via fetch into the schema variable, which makes the test flaky in CI; change the test to use a deterministic local fixture or a mocked fetch: add a committed JSON fixture of v5.json and in the test file load that fixture (or conditionally replace global fetch with a jest mock that returns the fixture when process.env.CI is set), keeping the existing fetch path for local development if desired; update the beforeAll to read the local file or use the mocked response for fetch so tests no longer depend on external network calls.packages/sql-editor/src/codemirror/extensions/completion.ts (1)
52-60: Potential React root memory leak with tooltip DOM elements.Each completion item creates a React root via
renderComponentToDomElementthat is never unmounted when CodeMirror discards the tooltip. While the utility documents this as intentional, frequent autocomplete usage could accumulate orphaned React roots.Consider whether the completion frequency warrants a cleanup mechanism, or whether a simpler non-React approach (plain DOM or HTML string) might be more appropriate for transient tooltips.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/sql-editor/src/codemirror/extensions/completion.ts` around lines 52 - 60, The tooltip DOM returned by info() currently uses renderComponentToDomElement to mount a React root for FunctionDocumentation (via renderComponentToDomElement) but never unmounts it, leaking roots when CodeMirror discards tooltips; modify the completion creation so either (A) you avoid mounting a persistent React root by returning a plain DOM/HTML string from info() instead of renderComponentToDomElement, or (B) keep a reference to the mounted root/node and ensure you call the React unmount API (e.g., root.unmount() or ReactDOM.unmountComponentAtNode) when CodeMirror removes the tooltip (hook into the tooltip/tooltipClose or completion UI lifecycle), updating the code paths around renderComponentToDomElement and the info() return to perform this cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/codemirror/__tests__/validate-json-schema.test.ts`:
- Around line 333-334: The test contains a duplicate assertion checking messages
for "Missing property"; update the second assertion to verify the other missing
property names instead of repeating the same check: locate the assertions using
the messages array (expect(messages.some((m) => m.includes('Missing
property')))...), and change them to assert that messages include "Missing
property 'street'" and "Missing property 'country'" (or similarly targeted
substrings) so both missing properties are individually verified.
In `@packages/codemirror/src/components/DiagnosticTooltip.tsx`:
- Around line 12-14: The conditional rendering for the diagnostic code in
DiagnosticTooltip.tsx uses a truthy check ({code && ...}) which hides valid
numeric code 0; update the guard to explicitly check for non-null/undefined
(e.g., code !== null && code !== undefined) so a code value of 0 is rendered
correctly, keeping the span markup and className unchanged and ensuring the
component's declared type (string | number | null) is respected.
In `@packages/utils/src/render-component.tsx`:
- Around line 27-33: renderComponentToDomElement currently returns only the
container DOM node causing React roots to be lost; change
renderComponentToDomElement (and the underlying renderToContainer usage) to
return an object with both the container DOM element and a destroy() callback
that unmounts the React root (i.e., call the appropriate root.unmount() /
cleanup created when mounting), then update its call sites (hover.ts create =>
({dom, destroy}), completion.ts info => ({dom, destroy}),
validate-json-schema.ts renderMessage => ({dom, destroy})) to use the returned
destroy for CodeMirror TooltipView/Completion.info cleanup.
---
Nitpick comments:
In `@packages/codemirror/__mocks__/`@sqlrooms/utils.ts:
- Around line 1-2: Update the file header comment to accurately reflect what is
mocked: mention that both the React rendering function and getMonospaceFont are
mocked (not just the React rendering function). Reference the mock file
packages/codemirror/__mocks__/@sqlrooms/utils.ts and the exported symbol
getMonospaceFont (and the React render function) so the header clearly states
both exports are mocked and the rest are no-ops.
In `@packages/codemirror/__tests__/vega-schema-integration.test.ts`:
- Around line 7-13: The beforeAll block currently fetches the Vega-Lite schema
from the network via fetch into the schema variable, which makes the test flaky
in CI; change the test to use a deterministic local fixture or a mocked fetch:
add a committed JSON fixture of v5.json and in the test file load that fixture
(or conditionally replace global fetch with a jest mock that returns the fixture
when process.env.CI is set), keeping the existing fetch path for local
development if desired; update the beforeAll to read the local file or use the
mocked response for fetch so tests no longer depend on external network calls.
In `@packages/codemirror/src/utils/create-json-schema-validator.ts`:
- Around line 11-22: The schemaRequestService implementation uses fetch without
a timeout, so slow/unresponsive schema URIs can hang validation; modify the
schemaRequestService async function to create an AbortController, start a timer
(e.g., timeoutMs constant or parameter) that calls controller.abort() after the
timeout, pass controller.signal to fetch(uri, { signal: controller.signal }),
clear the timer on success, and convert AbortError to a clear error message
before throwing; update references to schemaRequestService in
create-json-schema-validator to ensure the timeout is applied for all external
schema fetches.
In `@packages/codemirror/src/utils/validate-json-schema.ts`:
- Around line 92-106: The convertSeverity function currently compares vsSeverity
to magic numbers (1-4); replace those numeric literals with the corresponding
DiagnosticSeverity enum members (DiagnosticSeverity.Error,
DiagnosticSeverity.Warning, DiagnosticSeverity.Information,
DiagnosticSeverity.Hint) so the switch checks enum values and returns the same
string mapping ('error', 'warning', 'info', 'hint'), and keep the default return
as 'warning'; update the switch cases in convertSeverity to use the
DiagnosticSeverity enum identifiers to improve readability and maintainability.
- Around line 57-62: renderMessage currently creates a new React root for each
diagnostic by calling renderComponentToDomElement(DiagnosticTooltip, ...) and
never unmounts it, leaking roots; change renderMessage to create the React root
into the tooltip DOM element and return a cleanup/unmount callback (or reuse a
single root) so the React tree is unmounted when CodeMirror disposes the
tooltip; update both occurrences (renderMessage at lines shown and the similar
block around 76-80) to call the appropriate unmount method (or keep a shared
root) for the DiagnosticTooltip instead of leaving roots orphaned.
In `@packages/sql-editor/src/codemirror/extensions/completion.ts`:
- Around line 52-60: The tooltip DOM returned by info() currently uses
renderComponentToDomElement to mount a React root for FunctionDocumentation (via
renderComponentToDomElement) but never unmounts it, leaking roots when
CodeMirror discards tooltips; modify the completion creation so either (A) you
avoid mounting a persistent React root by returning a plain DOM/HTML string from
info() instead of renderComponentToDomElement, or (B) keep a reference to the
mounted root/node and ensure you call the React unmount API (e.g.,
root.unmount() or ReactDOM.unmountComponentAtNode) when CodeMirror removes the
tooltip (hook into the tooltip/tooltipClose or completion UI lifecycle),
updating the code paths around renderComponentToDomElement and the info() return
to perform this cleanup.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 395f3f11-edc2-4cbd-a95f-a2c1005cd7e9
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (18)
.gitignorepackages/codemirror/__mocks__/@sqlrooms/utils.tspackages/codemirror/__tests__/validate-json-schema.test.tspackages/codemirror/__tests__/vega-schema-integration.test.tspackages/codemirror/package.jsonpackages/codemirror/src/components/DiagnosticTooltip.tsxpackages/codemirror/src/components/JsonCodeMirrorEditor.tsxpackages/codemirror/src/extensions/json-schema-lint.tspackages/codemirror/src/themes/base-theme.tspackages/codemirror/src/themes/tooltip-theme.tspackages/codemirror/src/utils/create-json-schema-validator.tspackages/codemirror/src/utils/validate-json-schema.tspackages/sql-editor/src/codemirror/extensions/completion.tspackages/sql-editor/src/codemirror/extensions/hover.tspackages/utils/src/index.tspackages/utils/src/render-component.tsxpackages/utils/src/renderComponentToString.tsxpackages/vega/src/VegaEditAction.tsx
💤 Files with no reviewable changes (1)
- packages/utils/src/renderComponentToString.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/codemirror/src/utils/validate-json-schema.ts (1)
92-106: UseDiagnosticSeverityenum constants for clarity.Magic numbers reduce readability. The
DiagnosticSeverityenum is already imported.♻️ Proposed refactor
function convertSeverity( vsSeverity: DiagnosticSeverity | undefined, ): Diagnostic['severity'] { switch (vsSeverity) { - case 1: + case DiagnosticSeverity.Error: return 'error'; - case 2: + case DiagnosticSeverity.Warning: return 'warning'; - case 3: + case DiagnosticSeverity.Information: return 'info'; - case 4: + case DiagnosticSeverity.Hint: return 'hint'; default: return 'warning'; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 92 - 106, The switch in convertSeverity uses numeric magic values; replace the case labels 1,2,3,4 with the corresponding DiagnosticSeverity enum constants (e.g., DiagnosticSeverity.Error, DiagnosticSeverity.Warning, DiagnosticSeverity.Information, DiagnosticSeverity.Hint) so the mapping to Diagnostic['severity'] is clear and maintain the existing default return 'warning'; update only the case labels in convertSeverity to use these enum members.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/codemirror/src/utils/validate-json-schema.ts`:
- Around line 54-61: The Diagnostic.renderMessage implementation creates a React
root via renderComponentToDomElement (inside renderMessage) but never calls the
returned destroy/unmount, leaking roots; replace this by using
renderComponentToString to render DiagnosticTooltip to HTML and set that HTML
into a detached container (e.g., createElement('div').innerHTML =
renderComponentToString(DiagnosticTooltip, {message, code})) and return that
container DOM node from renderMessage (or alternatively track and call the
destroy function when diagnostics change); update the code paths that call
renderComponentToDomElement in renderMessage to use renderComponentToString and
avoid creating persistent React roots.
---
Nitpick comments:
In `@packages/codemirror/src/utils/validate-json-schema.ts`:
- Around line 92-106: The switch in convertSeverity uses numeric magic values;
replace the case labels 1,2,3,4 with the corresponding DiagnosticSeverity enum
constants (e.g., DiagnosticSeverity.Error, DiagnosticSeverity.Warning,
DiagnosticSeverity.Information, DiagnosticSeverity.Hint) so the mapping to
Diagnostic['severity'] is clear and maintain the existing default return
'warning'; update only the case labels in convertSeverity to use these enum
members.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3e713276-efaf-4657-91a0-c153b8a13be0
📒 Files selected for processing (7)
packages/codemirror/__mocks__/@sqlrooms/utils.tspackages/codemirror/__tests__/validate-json-schema.test.tspackages/codemirror/__tests__/vega-schema-integration.test.tspackages/codemirror/src/utils/validate-json-schema.tspackages/sql-editor/src/codemirror/extensions/completion.tspackages/sql-editor/src/codemirror/extensions/hover.tspackages/utils/src/render-component.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/codemirror/tests/vega-schema-integration.test.ts
- packages/sql-editor/src/codemirror/extensions/completion.ts
| renderMessage: () => { | ||
| const {dom} = renderComponentToDomElement(DiagnosticTooltip, { | ||
| message, | ||
| code, | ||
| }); | ||
|
|
||
| return dom; | ||
| }, |
There was a problem hiding this comment.
Memory leak: React root not cleaned up in renderMessage.
The destroy callback is destructured but discarded. Unlike TooltipView and Completion.info, CodeMirror's Diagnostic.renderMessage returns only a DOM node and doesn't provide a lifecycle hook for cleanup. Each time a diagnostic tooltip renders, a new React root is created but never unmounted.
Consider either:
- Using
renderComponentToStringand settinginnerHTML(simpler, no React root lifecycle) - Tracking roots externally and cleaning up when diagnostics change
💡 Option 1: Use renderComponentToString instead
renderMessage: () => {
- const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
+ const html = renderComponentToString(DiagnosticTooltip, {
message,
code,
});
-
- return dom;
+ const dom = document.createElement('div');
+ dom.innerHTML = html;
+ return dom;
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/codemirror/src/utils/validate-json-schema.ts` around lines 54 - 61,
The Diagnostic.renderMessage implementation creates a React root via
renderComponentToDomElement (inside renderMessage) but never calls the returned
destroy/unmount, leaking roots; replace this by using renderComponentToString to
render DiagnosticTooltip to HTML and set that HTML into a detached container
(e.g., createElement('div').innerHTML =
renderComponentToString(DiagnosticTooltip, {message, code})) and return that
container DOM node from renderMessage (or alternatively track and call the
destroy function when diagnostics change); update the code paths that call
renderComponentToDomElement in renderMessage to use renderComponentToString and
avoid creating persistent React roots.
There was a problem hiding this comment.
Looks like renderComponentToString could be a simple solution?
There was a problem hiding this comment.
@ilyabo Yes, renderComponentToString is the right fix here. It renders the component to an HTML string and immediately unmounts the React root before returning, so there's no persistent root to leak. Since DiagnosticTooltip is a purely presentational component, string rendering is perfectly sufficient.
Here's the concrete change for both renderMessage call sites in validate-json-schema.ts:
- import {renderComponentToDomElement} from '@sqlrooms/utils';
+ import {renderComponentToString} from '@sqlrooms/utils';
// Inside the vsDiagnostics loop:
renderMessage: () => {
- const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
+ const html = renderComponentToString(DiagnosticTooltip, {
message,
code,
});
- return dom;
+ const dom = document.createElement('div');
+ dom.innerHTML = html;
+ return dom;
},
// Inside the catch block:
renderMessage: () => {
- const {dom} = renderComponentToDomElement(DiagnosticTooltip, {
+ const html = renderComponentToString(DiagnosticTooltip, {
message,
});
- return dom;
+ const dom = document.createElement('div');
+ dom.innerHTML = html;
+ return dom;
},This also aligns well with how renderComponentToString is already used elsewhere in the codebase (e.g., the old hover/completion tooltips before this PR refactored them to use renderComponentToDomElement — but those have a destroy callback path, which renderMessage lacks).
There was a problem hiding this comment.
The renderMessage API requires a Node, not a string
There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
Co-authored-by: Claude Sonnet 4.5 <[email protected]>
This pull request significantly refactors JSON schema validation in the
codemirrorpackage, replacing the previousajv-based implementation with thevscode-json-languageservicefor more accurate, IDE-quality diagnostics and improved tooltip rendering. It also introduces a new diagnostic tooltip component, updates tooltip and theme styling, and improves test coverage for schema integration. Additionally, it standardizes the React-based rendering utility used for tooltips and documentation across the codebase.JSON Schema Validation Refactor:
ajvandjsonc-parserwithvscode-json-languageservicefor JSON schema validation, providing more precise and actionable diagnostics and leveraging VS Code’s language tooling. (packages/codemirror/src/utils/create-json-schema-validator.ts,packages/codemirror/src/utils/validate-json-schema.ts,packages/codemirror/package.json) [1] [2] [3]packages/codemirror/src/extensions/json-schema-lint.ts)Tooltip and Diagnostic UI Improvements:
DiagnosticTooltipReact component for consistent, styled diagnostic tooltips. (packages/codemirror/src/components/DiagnosticTooltip.tsx)packages/codemirror/src/themes/tooltip-theme.ts,packages/codemirror/src/themes/base-theme.ts) [1] [2]Testing and Mocking:
packages/codemirror/__tests__/vega-schema-integration.test.ts)packages/codemirror/__mocks__/@sqlrooms/utils.ts)Code Consistency and Cleanup:
renderComponentToDomElementfor rendering React components to DOM elements in tooltips and documentation, replacing previous usage of string-based rendering. (packages/sql-editor/src/codemirror/extensions/completion.ts,packages/sql-editor/src/codemirror/extensions/hover.ts) [1] [2] [3] [4]packages/codemirror/src/components/JsonCodeMirrorEditor.tsx) [1] [2]These changes modernize and unify schema validation, diagnostics, and tooltip rendering, resulting in a better developer and user experience.
Summary by CodeRabbit
New Features
Bug Fixes
Improvements