Skip to content

Commit 542d925

Browse files
waleedlatif1claude
andcommitted
fix(parsers): accept YAML document streams and JSON with comments
Kubernetes manifests, Helm output and CI fixtures hold several YAML documents separated by ---; js-yaml's single-document load rejected them outright. A stream now becomes one item per document. JSON files with comments or trailing commas (tsconfig, editor settings) parse leniently after strict parsing fails, with a warning in metadata. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 4534f51 commit 542d925

4 files changed

Lines changed: 113 additions & 3 deletions

File tree

‎apps/sim/lib/file-parsers/json-parser.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ describe('JSON parser complexity limits', () => {
6161
expect(result.metadata?.warning).toMatch(/Windows-1252/)
6262
})
6363

64+
it('parses JSON with comments and trailing commas leniently with a warning', async () => {
65+
const jsonc =
66+
'{\n // strict later\n "compilerOptions": { "strict": true, /* todo */ "target": "esnext", },\n "url": "http://example.com/a//b",\n}\n'
67+
const result = await parseJSONBuffer(Buffer.from(jsonc))
68+
const parsed = JSON.parse(result.content) as {
69+
compilerOptions: { target: string }
70+
url: string
71+
}
72+
73+
expect(parsed.compilerOptions.target).toBe('esnext')
74+
expect(parsed.url).toBe('http://example.com/a//b')
75+
expect(result.metadata?.warning).toContain('comments')
76+
})
77+
78+
it('still rejects JSON that is invalid even after comment stripping', async () => {
79+
await expect(parseJSONBuffer(Buffer.from('{ "a": [1, 2 }'))).rejects.toMatchObject({
80+
code: 'invalid_format',
81+
})
82+
})
83+
6484
it('parses BOM-prefixed JSON Lines', async () => {
6585
const result = await parseJSONLBuffer(
6686
Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('{"a":1}\n{"a":2}')])

‎apps/sim/lib/file-parsers/json-parser.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,45 @@ function buildJsonResult(jsonData: unknown, decoded: DecodedText): FileParseResu
174174
* `toString('utf-8')` read used to reject every BOM-prefixed file from Windows
175175
* editors, and a Windows-1252 file silently lost its accented characters.
176176
*/
177+
const JSONC_WARNING = 'File is JSON with comments or trailing commas; parsed leniently'
178+
179+
/**
180+
* Removes `//` and `/* *\/` comments and trailing commas outside string
181+
* literals, so a `tsconfig.json`, `.vscode` settings file or `devcontainer.json`
182+
* — JSON with comments, which editors accept — parses like plain JSON. Runs
183+
* only after strict parsing has failed, so valid JSON never goes through it.
184+
*/
185+
export function stripJsonComments(text: string): string {
186+
let out = ''
187+
let index = 0
188+
while (index < text.length) {
189+
const char = text[index]
190+
if (char === '"') {
191+
let end = index + 1
192+
while (end < text.length && text[end] !== '"') {
193+
if (text[end] === '\\') end++
194+
end++
195+
}
196+
out += text.slice(index, end + 1)
197+
index = end + 1
198+
continue
199+
}
200+
if (char === '/' && text[index + 1] === '/') {
201+
const end = text.indexOf('\n', index)
202+
index = end === -1 ? text.length : end
203+
continue
204+
}
205+
if (char === '/' && text[index + 1] === '*') {
206+
const end = text.indexOf('*/', index + 2)
207+
index = end === -1 ? text.length : end + 2
208+
continue
209+
}
210+
out += char
211+
index++
212+
}
213+
return out.replace(/,(\s*[}\]])/g, '$1')
214+
}
215+
177216
function parseJsonContent(buffer: Uint8Array): FileParseResult {
178217
const decoded = decodeTextBuffer(buffer)
179218
try {
@@ -183,6 +222,8 @@ function parseJsonContent(buffer: Uint8Array): FileParseResult {
183222
if (!(error instanceof SyntaxError)) {
184223
throw new FileParserError('runtime_failure', 'JSON processing failed unexpectedly', error)
185224
}
225+
const lenient = parseJsonWithComments(decoded)
226+
if (lenient) return lenient
186227
throw new FileParserError(
187228
'invalid_format',
188229
`Invalid JSON: ${getErrorMessage(error, 'Unknown error')}`,
@@ -191,6 +232,18 @@ function parseJsonContent(buffer: Uint8Array): FileParseResult {
191232
}
192233
}
193234

235+
function parseJsonWithComments(decoded: DecodedText): FileParseResult | undefined {
236+
let value: unknown
237+
try {
238+
value = JSON.parse(stripJsonComments(decoded.text))
239+
} catch {
240+
return undefined
241+
}
242+
const result = buildJsonResult(value, decoded)
243+
const warning = [decoded.warning, JSONC_WARNING].filter(Boolean).join('; ')
244+
return { ...result, metadata: { ...result.metadata, warning } }
245+
}
246+
194247
/** Parse a JSON file. */
195248
export async function parseJSON(filePath: string): Promise<FileParseResult> {
196249
const fs = await import('fs/promises')

‎apps/sim/lib/file-parsers/yaml-parser.test.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,28 @@ describe('parseYAMLBuffer', () => {
7070
await expect(parseYAMLBuffer(Buffer.from(bomb))).rejects.toBeInstanceOf(YamlComplexityError)
7171
})
7272

73+
it('parses a multi-document stream as one document per item', async () => {
74+
const stream =
75+
'apiVersion: v1\nkind: Service\nmetadata:\n name: web\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: web\n'
76+
const result = await parseYAMLBuffer(Buffer.from(stream))
77+
const parsed = JSON.parse(result.content) as Array<{ kind: string }>
78+
79+
expect(parsed.map((document) => document.kind)).toEqual(['Service', 'Deployment'])
80+
expect(result.metadata).toMatchObject({
81+
type: 'yaml',
82+
isArray: true,
83+
itemCount: 2,
84+
documentCount: 2,
85+
})
86+
})
87+
88+
it('keeps a single document unwrapped and skips empty documents in a stream', async () => {
89+
const result = await parseYAMLBuffer(Buffer.from('---\nname: solo\n---\n'))
90+
91+
expect(JSON.parse(result.content)).toEqual({ name: 'solo' })
92+
expect(result.metadata).toMatchObject({ isArray: false, documentCount: 1 })
93+
})
94+
7395
it('surfaces malformed YAML as an Invalid YAML error', async () => {
7496
await expect(parseYAMLBuffer(Buffer.from('key: "unterminated\n'))).rejects.toThrow(
7597
/Invalid YAML/

‎apps/sim/lib/file-parsers/yaml-parser.ts‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export function assertYamlWithinLimits(root: unknown): number {
5353
* Parse a YAML value into the shared `FileParseResult` shape after validating
5454
* that its expanded form stays within safe complexity limits.
5555
*/
56-
function buildYamlResult(yamlData: unknown, decoded: DecodedText): FileParseResult {
56+
function buildYamlResult(
57+
yamlData: unknown,
58+
decoded: DecodedText,
59+
documentCount: number
60+
): FileParseResult {
5761
if (yamlData === undefined) {
5862
throw new FileParserError('empty_input', 'Empty YAML input provided')
5963
}
@@ -67,6 +71,7 @@ function buildYamlResult(yamlData: unknown, decoded: DecodedText): FileParseResu
6771
keys: Array.isArray(yamlData) ? [] : Object.keys((yamlData as Record<string, unknown>) || {}),
6872
itemCount: Array.isArray(yamlData) ? yamlData.length : undefined,
6973
depth,
74+
documentCount,
7075
encoding: decoded.encoding,
7176
...(decoded.warning ? { warning: decoded.warning } : {}),
7277
}
@@ -96,8 +101,18 @@ export async function parseYAMLBuffer(buffer: Buffer): Promise<FileParseResult>
96101
const decoded = decodeTextBuffer(buffer)
97102

98103
try {
99-
const yamlData = yaml.load(decoded.text)
100-
return buildYamlResult(yamlData, decoded)
104+
/**
105+
* A YAML file is a stream: Kubernetes manifests, Helm output and CI
106+
* fixtures routinely hold several documents separated by `---`. A single
107+
* document keeps its own shape; a multi-document stream becomes an array of
108+
* documents, which the JSON/YAML chunker then splits one document per item.
109+
*/
110+
const documents = yaml
111+
.loadAll(decoded.text)
112+
.filter((document) => document !== undefined && document !== null)
113+
const yamlData =
114+
documents.length === 1 ? documents[0] : documents.length === 0 ? undefined : documents
115+
return buildYamlResult(yamlData, decoded, documents.length)
101116
} catch (error) {
102117
if (error instanceof FileParserError) throw error
103118
throw new FileParserError(

0 commit comments

Comments
 (0)