From b77c256943b6a5f23d78669788aa9d8324ebe927 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 7 Apr 2026 15:31:10 +0900 Subject: [PATCH 1/2] fix(jsonc): strip BOM before parsing (#3164) --- src/shared/jsonc-parser.test.ts | 59 ++++++++++++++++++++++++++------- src/shared/jsonc-parser.ts | 8 +++-- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/shared/jsonc-parser.test.ts b/src/shared/jsonc-parser.test.ts index aacd69113..279db1fc5 100644 --- a/src/shared/jsonc-parser.test.ts +++ b/src/shared/jsonc-parser.test.ts @@ -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", () => { diff --git a/src/shared/jsonc-parser.ts b/src/shared/jsonc-parser.ts index 818d0a63b..da1e0d98c 100644 --- a/src/shared/jsonc-parser.ts +++ b/src/shared/jsonc-parser.ts @@ -9,12 +9,16 @@ export interface JsoncParseResult { 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(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(content: string): T { export function parseJsoncSafe(content: string): JsoncParseResult { const errors: ParseError[] = [] - const data = parse(content, errors, { + const data = parse(stripBom(content), errors, { allowTrailingComma: true, disallowComments: false, }) as T | null From 360fd3211d49056e9f14f342e2e4585aaedd253d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 7 Apr 2026 15:17:32 +0900 Subject: [PATCH 2/2] ci: trigger workflows