2025-12-28 00:45:17 +09:00
|
|
|
import { existsSync, readFileSync } from "node:fs"
|
2026-03-23 18:11:01 +09:00
|
|
|
import { join } from "node:path"
|
2025-12-28 00:45:17 +09:00
|
|
|
import { parse, ParseError, printParseErrorCode } from "jsonc-parser"
|
|
|
|
|
|
2026-04-03 17:15:08 +09:00
|
|
|
import { CONFIG_BASENAME, LEGACY_CONFIG_BASENAME } from "./plugin-identity"
|
|
|
|
|
|
2025-12-28 00:45:17 +09:00
|
|
|
export interface JsoncParseResult<T> {
|
|
|
|
|
data: T | null
|
|
|
|
|
errors: Array<{ message: string; offset: number; length: number }>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseJsonc<T = unknown>(content: string): T {
|
2026-04-07 14:48:30 +09:00
|
|
|
// Strip UTF-8 BOM if present (Windows UTF-8 with BOM files)
|
|
|
|
|
content = content.replace(/^\uFEFF/, "")
|
|
|
|
|
|
2025-12-28 00:45:17 +09:00
|
|
|
const errors: ParseError[] = []
|
|
|
|
|
const result = parse(content, errors, {
|
|
|
|
|
allowTrailingComma: true,
|
|
|
|
|
disallowComments: false,
|
|
|
|
|
}) as T
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
const errorMessages = errors
|
|
|
|
|
.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`)
|
|
|
|
|
.join(", ")
|
|
|
|
|
throw new SyntaxError(`JSONC parse error: ${errorMessages}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function parseJsoncSafe<T = unknown>(content: string): JsoncParseResult<T> {
|
|
|
|
|
const errors: ParseError[] = []
|
|
|
|
|
const data = parse(content, errors, {
|
|
|
|
|
allowTrailingComma: true,
|
|
|
|
|
disallowComments: false,
|
|
|
|
|
}) as T | null
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data: errors.length > 0 ? null : data,
|
|
|
|
|
errors: errors.map((e) => ({
|
|
|
|
|
message: printParseErrorCode(e.error),
|
|
|
|
|
offset: e.offset,
|
|
|
|
|
length: e.length,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function readJsoncFile<T = unknown>(filePath: string): T | null {
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(filePath, "utf-8")
|
|
|
|
|
return parseJsonc<T>(content)
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function detectConfigFile(basePath: string): {
|
|
|
|
|
format: "json" | "jsonc" | "none"
|
|
|
|
|
path: string
|
|
|
|
|
} {
|
|
|
|
|
const jsoncPath = `${basePath}.jsonc`
|
|
|
|
|
const jsonPath = `${basePath}.json`
|
|
|
|
|
|
|
|
|
|
if (existsSync(jsoncPath)) {
|
|
|
|
|
return { format: "jsonc", path: jsoncPath }
|
|
|
|
|
}
|
|
|
|
|
if (existsSync(jsonPath)) {
|
|
|
|
|
return { format: "json", path: jsonPath }
|
|
|
|
|
}
|
|
|
|
|
return { format: "none", path: jsonPath }
|
|
|
|
|
}
|
2026-03-23 18:11:01 +09:00
|
|
|
|
|
|
|
|
export function detectPluginConfigFile(dir: string): {
|
|
|
|
|
format: "json" | "jsonc" | "none"
|
|
|
|
|
path: string
|
2026-04-03 17:15:08 +09:00
|
|
|
legacyPath?: string
|
2026-03-23 18:11:01 +09:00
|
|
|
} {
|
2026-04-03 17:15:08 +09:00
|
|
|
const canonicalResult = detectConfigFile(join(dir, CONFIG_BASENAME))
|
|
|
|
|
const legacyResult = detectConfigFile(join(dir, LEGACY_CONFIG_BASENAME))
|
|
|
|
|
|
|
|
|
|
if (canonicalResult.format !== "none") {
|
|
|
|
|
return {
|
|
|
|
|
...canonicalResult,
|
|
|
|
|
legacyPath: legacyResult.format !== "none" ? legacyResult.path : undefined,
|
|
|
|
|
}
|
2026-03-23 18:11:01 +09:00
|
|
|
}
|
2026-04-03 17:15:08 +09:00
|
|
|
|
|
|
|
|
if (legacyResult.format !== "none") {
|
|
|
|
|
return legacyResult
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { format: "none", path: join(dir, `${CONFIG_BASENAME}.json`) }
|
2026-03-23 18:11:01 +09:00
|
|
|
}
|