Merge pull request #3178 from code-yeongyu/fix/issue-3164

fix(jsonc): strip UTF-8 BOM before parsing JSONC files (#3164)
This commit is contained in:
YeonGyu-Kim
2026-04-07 15:43:55 +09:00
committed by GitHub
2 changed files with 53 additions and 14 deletions
+47 -12
View File
@@ -140,10 +140,9 @@ describe("parseJsonc", () => {
expect(() => parseJsonc(invalid)).toThrow() expect(() => parseJsonc(invalid)).toThrow()
}) })
test("parses JSONC with UTF-8 BOM (Windows BOM files)", () => { test("parses content with UTF-8 BOM prefix", () => {
// given - JSON with UTF-8 BOM marker // given
const bom = "\uFEFF" const jsonc = `\uFEFF{"key": "value"}`
const jsonc = `${bom}{ "key": "value" }`
// when // when
const result = parseJsonc<{ key: string }>(jsonc) const result = parseJsonc<{ key: string }>(jsonc)
@@ -152,19 +151,20 @@ describe("parseJsonc", () => {
expect(result.key).toBe("value") expect(result.key).toBe("value")
}) })
test("parses JSONC with BOM and comments", () => { test("parses commented JSONC with UTF-8 BOM prefix", () => {
// given - JSONC with UTF-8 BOM and comments // given
const bom = "\uFEFF" const jsonc = `\uFEFF{
const jsonc = `${bom}{ // Windows-saved file with BOM
// Windows editor saved with BOM "$schema": "https://opencode.ai/config.json",
"key": "value" "plugin": ["oh-my-openagent@3.15.3"],
}` }`
// when // when
const result = parseJsonc<{ key: string }>(jsonc) const result = parseJsonc<{ $schema: string; plugin: string[] }>(jsonc)
// then // then
expect(result.key).toBe("value") expect(result.$schema).toBe("https://opencode.ai/config.json")
expect(result.plugin).toEqual(["oh-my-openagent@3.15.3"])
}) })
}) })
@@ -193,6 +193,19 @@ describe("parseJsoncSafe", () => {
expect(result.data).toBeNull() expect(result.data).toBeNull()
expect(result.errors.length).toBeGreaterThan(0) expect(result.errors.length).toBeGreaterThan(0)
}) })
test("returns data when content has UTF-8 BOM prefix", () => {
// given
const jsonc = `\uFEFF{"key": "value"}`
// when
const result = parseJsoncSafe<{ key: string }>(jsonc)
// then
expect(result.errors).toHaveLength(0)
expect(result.data).not.toBeNull()
expect(result.data?.key).toBe("value")
})
}) })
describe("readJsoncFile", () => { describe("readJsoncFile", () => {
@@ -242,6 +255,28 @@ describe("readJsoncFile", () => {
rmSync(testDir, { recursive: true, force: true }) rmSync(testDir, { recursive: true, force: true })
}) })
test("reads JSONC file written with UTF-8 BOM (Windows scenario)", () => {
// given
if (!existsSync(testDir)) mkdirSync(testDir, { recursive: true })
const bomBytes = Buffer.from([0xef, 0xbb, 0xbf])
const jsonBytes = Buffer.from(`{
// Created on Windows with BOM
"$schema": "https://opencode.ai/config.json",
"plugin": ["oh-my-openagent@3.15.3"]
}`)
writeFileSync(testFile, Buffer.concat([bomBytes, jsonBytes]))
// when
const result = readJsoncFile<{ $schema: string; plugin: string[] }>(testFile)
// then
expect(result).not.toBeNull()
expect(result?.$schema).toBe("https://opencode.ai/config.json")
expect(result?.plugin).toEqual(["oh-my-openagent@3.15.3"])
rmSync(testDir, { recursive: true, force: true })
})
}) })
describe("detectConfigFile", () => { describe("detectConfigFile", () => {
+6 -2
View File
@@ -9,12 +9,16 @@ export interface JsoncParseResult<T> {
errors: Array<{ message: string; offset: number; length: number }> errors: Array<{ message: string; offset: number; length: number }>
} }
function stripBom(content: string): string {
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content
}
export function parseJsonc<T = unknown>(content: string): T { export function parseJsonc<T = unknown>(content: string): T {
// Strip UTF-8 BOM if present (Windows UTF-8 with BOM files) // Strip UTF-8 BOM if present (Windows UTF-8 with BOM files)
content = content.replace(/^\uFEFF/, "") content = content.replace(/^\uFEFF/, "")
const errors: ParseError[] = [] const errors: ParseError[] = []
const result = parse(content, errors, { const result = parse(stripBom(content), errors, {
allowTrailingComma: true, allowTrailingComma: true,
disallowComments: false, disallowComments: false,
}) as T }) as T
@@ -31,7 +35,7 @@ export function parseJsonc<T = unknown>(content: string): T {
export function parseJsoncSafe<T = unknown>(content: string): JsoncParseResult<T> { export function parseJsoncSafe<T = unknown>(content: string): JsoncParseResult<T> {
const errors: ParseError[] = [] const errors: ParseError[] = []
const data = parse(content, errors, { const data = parse(stripBom(content), errors, {
allowTrailingComma: true, allowTrailingComma: true,
disallowComments: false, disallowComments: false,
}) as T | null }) as T | null