Skip to content

Commit 38bf301

Browse files
committed
fix: JSONC comment stripping now respects string boundaries
The naive regex /\/\/.*$/gm was incorrectly stripping content after // inside string values (like URLs). This caused config files with http:// or https:// URLs to become malformed JSON. Added a proper state-machine based JSONC stripper that: - Tracks string boundaries - Handles escaped quotes - Preserves // and /* inside quoted strings - Only strips actual comments outside strings Fixes #11
1 parent 9519f6d commit 38bf301

File tree

3 files changed

+76
-6
lines changed

3 files changed

+76
-6
lines changed

src/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
33
import { join } from "node:path";
44
import { homedir } from "node:os";
55
import * as readline from "node:readline";
6+
import { stripJsoncComments } from "./services/jsonc.js";
67

78
const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
89
const OPENCODE_COMMAND_DIR = join(OPENCODE_CONFIG_DIR, "command");
@@ -200,8 +201,7 @@ function addPluginToConfig(configPath: string): boolean {
200201
return true;
201202
}
202203

203-
// Parse JSONC (strip comments for parsing)
204-
const jsonContent = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
204+
const jsonContent = stripJsoncComments(content);
205205
let config: Record<string, unknown>;
206206

207207
try {

src/config.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, readFileSync } from "node:fs";
22
import { join } from "node:path";
33
import { homedir } from "node:os";
4+
import { stripJsoncComments } from "./services/jsonc.js";
45

56
const CONFIG_DIR = join(homedir(), ".config", "opencode");
67
const CONFIG_FILES = [
@@ -34,10 +35,7 @@ function loadConfig(): SupermemoryConfig {
3435
if (existsSync(path)) {
3536
try {
3637
const content = readFileSync(path, "utf-8");
37-
// Strip comments for JSONC
38-
const json = content
39-
.replace(/\/\/.*$/gm, "")
40-
.replace(/\/\*[\s\S]*?\*\//g, "");
38+
const json = stripJsoncComments(content);
4139
return JSON.parse(json) as SupermemoryConfig;
4240
} catch {
4341
// Invalid config, use defaults

src/services/jsonc.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Strips comments from JSONC content while respecting string boundaries.
3+
* Handles // and /* comments, URLs in strings, and escaped quotes.
4+
*/
5+
export function stripJsoncComments(content: string): string {
6+
let result = "";
7+
let i = 0;
8+
let inString = false;
9+
let inSingleLineComment = false;
10+
let inMultiLineComment = false;
11+
12+
while (i < content.length) {
13+
const char = content[i];
14+
const nextChar = content[i + 1];
15+
16+
if (!inSingleLineComment && !inMultiLineComment) {
17+
if (char === '"' && (i === 0 || content[i - 1] !== "\\")) {
18+
inString = !inString;
19+
result += char;
20+
i++;
21+
continue;
22+
}
23+
}
24+
25+
if (inString) {
26+
result += char;
27+
i++;
28+
continue;
29+
}
30+
31+
if (!inSingleLineComment && !inMultiLineComment) {
32+
if (char === "/" && nextChar === "/") {
33+
inSingleLineComment = true;
34+
i += 2;
35+
continue;
36+
}
37+
38+
if (char === "/" && nextChar === "*") {
39+
inMultiLineComment = true;
40+
i += 2;
41+
continue;
42+
}
43+
}
44+
45+
if (inSingleLineComment) {
46+
if (char === "\n") {
47+
inSingleLineComment = false;
48+
result += char;
49+
}
50+
i++;
51+
continue;
52+
}
53+
54+
if (inMultiLineComment) {
55+
if (char === "*" && nextChar === "/") {
56+
inMultiLineComment = false;
57+
i += 2;
58+
continue;
59+
}
60+
if (char === "\n") {
61+
result += char;
62+
}
63+
i++;
64+
continue;
65+
}
66+
67+
result += char;
68+
i++;
69+
}
70+
71+
return result;
72+
}

0 commit comments

Comments
 (0)