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()
})
test("parses JSONC with UTF-8 BOM (Windows BOM files)", () => {
// given - JSON with UTF-8 BOM marker
const bom = "\uFEFF"
const jsonc = `${bom}{ "key": "value" }`
test("parses content with UTF-8 BOM prefix", () => {
// given
const jsonc = `\uFEFF{"key": "value"}`
// when
const result = parseJsonc<{ key: string }>(jsonc)
@@ -152,19 +151,20 @@ describe("parseJsonc", () => {
expect(result.key).toBe("value")
})
test("parses JSONC with BOM and comments", () => {
// given - JSONC with UTF-8 BOM and comments
const bom = "\uFEFF"
const jsonc = `${bom}{
// Windows editor saved with BOM
"key": "value"
test("parses commented JSONC with UTF-8 BOM prefix", () => {
// given
const jsonc = `\uFEFF{
// Windows-saved file with BOM
"$schema": "https://opencode.ai/config.json",
"plugin": ["oh-my-openagent@3.15.3"],
}`
// when
const result = parseJsonc<{ key: string }>(jsonc)
const result = parseJsonc<{ $schema: string; plugin: string[] }>(jsonc)
// 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.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", () => {
@@ -242,6 +255,28 @@ describe("readJsoncFile", () => {
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", () => {
+6 -2
View File
@@ -9,12 +9,16 @@ export interface JsoncParseResult<T> {
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 {
// Strip UTF-8 BOM if present (Windows UTF-8 with BOM files)
content = content.replace(/^\uFEFF/, "")
const errors: ParseError[] = []
const result = parse(content, errors, {
const result = parse(stripBom(content), errors, {
allowTrailingComma: true,
disallowComments: false,
}) as T
@@ -31,7 +35,7 @@ export function parseJsonc<T = unknown>(content: string): T {
export function parseJsoncSafe<T = unknown>(content: string): JsoncParseResult<T> {
const errors: ParseError[] = []
const data = parse(content, errors, {
const data = parse(stripBom(content), errors, {
allowTrailingComma: true,
disallowComments: false,
}) as T | null