Skip to content

Commit e1a4a70

Browse files
Replace strip-json-comments with jsonc-parser for better error reporting
1 parent e1890d0 commit e1a4a70

13 files changed

Lines changed: 132 additions & 34 deletions

File tree

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"dotenv": "^17.2.3",
2424
"eventsource": "^4.1.0",
2525
"hono": "^4.11.7",
26-
"strip-json-comments": "^3.1.1",
26+
"jsonc-parser": "^3.3.1",
2727
"web-push": "^3.6.7",
2828
"zod": "^4.1.12"
2929
},

backend/src/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import {
5454
ENV
5555
} from '@opencode-manager/shared/config/env'
5656
import { OpenCodeConfigSchema } from '@opencode-manager/shared/schemas'
57-
import stripJsonComments from 'strip-json-comments'
57+
import { parse as parseJsonc } from 'jsonc-parser'
5858

5959
const { PORT, HOST } = ENV.SERVER
6060
const DB_PATH = getDatabasePath()
@@ -90,7 +90,7 @@ async function ensureDefaultConfigExists(): Promise<void> {
9090
logger.info(`Found workspace config at ${workspaceConfigPath}, syncing to database...`)
9191
try {
9292
const rawContent = await readFileContent(workspaceConfigPath)
93-
const parsed = JSON.parse(stripJsonComments(rawContent))
93+
const parsed = parseJsonc(rawContent)
9494
const validation = OpenCodeConfigSchema.safeParse(parsed)
9595

9696
if (!validation.success) {
@@ -123,7 +123,7 @@ async function ensureDefaultConfigExists(): Promise<void> {
123123
logger.info(`Found home config at ${homeConfigPath}, importing...`)
124124
try {
125125
const rawContent = await readFileContent(homeConfigPath)
126-
const parsed = JSON.parse(stripJsonComments(rawContent))
126+
const parsed = parseJsonc(rawContent)
127127
const validation = OpenCodeConfigSchema.safeParse(parsed)
128128

129129
if (validation.success) {

backend/src/services/settings.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Database } from 'bun:sqlite'
22
import { unlinkSync, existsSync } from 'fs'
33
import { getOpenCodeConfigFilePath } from '@opencode-manager/shared/config/env'
44
import { logger } from '../utils/logger'
5-
import stripJsonComments from 'strip-json-comments'
5+
import { parseJsonc } from '@opencode-manager/shared/utils'
66
import type {
77
UserPreferences,
88
SettingsResponse,
@@ -25,9 +25,6 @@ interface OpenCodeConfigResponseWithRaw {
2525
defaultConfig: OpenCodeConfigWithRaw | null
2626
}
2727

28-
function parseJsonc(content: string): unknown {
29-
return JSON.parse(stripJsonComments(content))
30-
}
3128

3229
export class SettingsService {
3330
private static lastKnownGoodConfigContent: string | null = null

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"date-fns": "^4.1.0",
3737
"diff": "^8.0.2",
3838
"highlight.js": "^11.11.1",
39+
"jsonc-parser": "^3.3.1",
3940
"lucide-react": "^0.546.0",
4041
"mermaid": "^11.12.2",
4142
"react": "^19.1.1",
@@ -47,7 +48,6 @@
4748
"rehype-raw": "^7.0.0",
4849
"remark-gfm": "^4.0.1",
4950
"sonner": "^2.0.7",
50-
"strip-json-comments": "^5.0.3",
5151
"tailwind-merge": "^3.3.1",
5252
"zod": "^4.1.12",
5353
"zustand": "^5.0.8"

frontend/src/api/fetchWrapper.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,34 @@ interface FetchWrapperOptions extends RequestInit {
88
params?: Record<string, string | number | boolean | undefined>
99
}
1010

11+
function formatDetails(details: unknown): string | undefined {
12+
if (Array.isArray(details)) {
13+
return details
14+
.map((d) => {
15+
if (typeof d !== 'object' || d === null) return null
16+
const path = Array.isArray((d as Record<string, unknown>).path)
17+
? ((d as Record<string, unknown>).path as string[])
18+
: undefined
19+
const message = typeof (d as Record<string, unknown>).message === 'string'
20+
? (d as Record<string, unknown>).message as string
21+
: undefined
22+
return path?.length ? `${path.join('.')}: ${message}` : message
23+
})
24+
.filter(Boolean)
25+
.join('; ')
26+
}
27+
if (typeof details === 'string') return details
28+
return undefined
29+
}
30+
1131
async function handleResponse(response: Response): Promise<never> {
1232
const data: ApiErrorResponse = await response.json().catch(() => ({ error: 'An error occurred' }))
33+
const detail = data.detail || formatDetails(data.details)
1334
throw new FetchError(
1435
data.error || 'Request failed',
1536
response.status,
1637
data.code,
17-
data.detail
38+
detail
1839
)
1940
}
2041

frontend/src/components/settings/OpenCodeConfigEditor.tsx

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Loader2 } from 'lucide-react'
55
import { Dialog, DialogContent, DialogHeader, DialogFooter, DialogTitle } from '@/components/ui/dialog'
66
import type { OpenCodeConfig } from '@/api/types/settings'
77
import { parseJsonc } from '@/lib/jsonc'
8+
import { FetchError } from '@/api/fetchWrapper'
89

910
interface OpenCodeConfigEditorProps {
1011
config: OpenCodeConfig | null
@@ -44,28 +45,46 @@ export function OpenCodeConfigEditor({
4445
if (!config) return
4546

4647
try {
47-
const parsedContent = parseJsonc<Record<string, unknown>>(editConfigContent)
48-
49-
const forbiddenFields = ['id', 'createdAt', 'updatedAt']
50-
const foundForbidden = forbiddenFields.filter(field => field in parsedContent)
51-
if (foundForbidden.length > 0) {
52-
throw new Error(`Invalid fields found: ${foundForbidden.join(', ')}. These fields are managed automatically.`)
53-
}
54-
48+
parseJsonc<Record<string, unknown>>(editConfigContent)
5549
await onUpdate(editConfigContent)
5650
onClose()
5751
} catch (error) {
5852
if (error instanceof SyntaxError) {
59-
const match = error.message.match(/line (\d+)/i)
60-
const line = match ? parseInt(match[1]) : null
53+
const lineMatch = error.message.match(/line\s+(\d+)/i)
54+
const line = lineMatch ? parseInt(lineMatch[1]) : null
6155
setEditErrorLine(line)
62-
setEditError('Invalid JSON/JSONC format')
56+
if (line && editTextareaRef.current) {
57+
highlightErrorLine(editTextareaRef.current, line)
58+
}
59+
setEditError(`Invalid JSON/JSONC: ${error.message}`)
60+
} else if (error instanceof FetchError) {
61+
setEditError(error.detail || error.message)
62+
} else if (error instanceof Error) {
63+
setEditError(error.message)
6364
} else {
64-
setEditError('Failed to save. Please check your changes and try again.')
65+
setEditError('Failed to save configuration')
6566
}
6667
}
6768
}
6869

70+
const highlightErrorLine = (textarea: HTMLTextAreaElement, line: number) => {
71+
const lines = textarea.value.split('\n')
72+
if (line > lines.length) return
73+
74+
let charIndex = 0
75+
for (let i = 0; i < line - 1; i++) {
76+
charIndex += lines[i].length + 1
77+
}
78+
79+
textarea.focus()
80+
textarea.setSelectionRange(charIndex, charIndex + lines[line - 1].length)
81+
82+
// Scroll to make the error line visible
83+
const lineHeight = textarea.scrollHeight / lines.length
84+
const targetPosition = lineHeight * (line - 1)
85+
textarea.scrollTop = targetPosition - textarea.clientHeight / 2 + lineHeight / 2
86+
}
87+
6988
if (!config) return null
7089

7190
return (
@@ -87,7 +106,7 @@ export function OpenCodeConfigEditor({
87106
setEditError('')
88107
setEditErrorLine(null)
89108
}}
90-
className="flex-1 font-mono text-[16px] sm:text-xs md:text-sm resize-none h-full rounded-none sm:rounded-md"
109+
className={`flex-1 font-mono text-[16px] sm:text-xs md:text-sm resize-none h-full rounded-none sm:rounded-md ${editErrorLine ? 'error-highlight' : ''}`}
91110
/>
92111
{editError && (
93112
<div className="absolute bottom-0 left-0 right-0 bg-background/95 border-t p-2 sm:p-3">

frontend/src/index.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,24 @@ body {
374374
.animate-progress {
375375
animation: progress 2s ease-in-out infinite;
376376
}
377+
378+
/* Error line highlighting - red-orange selection color */
379+
.error-highlight::selection {
380+
background-color: #fecaca;
381+
color: #991b1b;
382+
}
383+
384+
.dark .error-highlight::selection {
385+
background-color: #7f1d1d;
386+
color: #fecaca;
387+
}
388+
389+
.error-highlight::-moz-selection {
390+
background-color: #fecaca;
391+
color: #991b1b;
392+
}
393+
394+
.dark .error-highlight::-moz-selection {
395+
background-color: #7f1d1d;
396+
color: #fecaca;
397+
}

frontend/src/lib/jsonc.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import stripJsonComments from 'strip-json-comments'
2-
3-
export function parseJsonc<T = unknown>(content: string): T {
4-
try {
5-
return JSON.parse(stripJsonComments(content)) as T
6-
} catch (e) {
7-
throw new Error(`Failed to parse JSONC: ${e instanceof Error ? e.message : String(e)}`)
8-
}
9-
}
1+
export { parseJsonc } from '@opencode-manager/shared/utils'
102

113
export function hasJsoncComments(content: string): boolean {
124
return content.split('\n').some(line => {

shared/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"./config": "./src/config/index.ts",
1414
"./config/defaults": "./src/config/defaults.ts",
1515
"./config/env": "./src/config/env.ts",
16-
"./config/client": "./src/config/client.ts"
16+
"./config/client": "./src/config/client.ts",
17+
"./utils": "./src/utils/index.ts"
1718
},
1819
"dependencies": {
1920
"zod": "^4.1.12"

shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './config'
22
export * from './schemas'
33
export * from './types'
4+
export * from './utils'

0 commit comments

Comments
 (0)