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:
@@ -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", () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user