From 83797a772a3af6e5753e9eccc4b48edd61e52c0a Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 8 Dec 2025 18:35:20 +0530 Subject: [PATCH 1/5] fix(frontmatter): improve duplicate frontmatter key error handling --- .../decap-cms-core/src/formats/frontmatter.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/decap-cms-core/src/formats/frontmatter.ts b/packages/decap-cms-core/src/formats/frontmatter.ts index 814ede8e3d6b..11b3f75a3a83 100644 --- a/packages/decap-cms-core/src/formats/frontmatter.ts +++ b/packages/decap-cms-core/src/formats/frontmatter.ts @@ -100,6 +100,31 @@ export class FrontmatterFormatter { fromFile(content: string) { const format = this.format || inferFrontmatterFormat(content); + + // Duplicate key detection for yaml frontmatter + { + const lines = content.split('\n'); + // Detect duplicate keys in frontmatter (YAML only) + if (!this.format || this.format.language === 'yaml') { + const keyCounts: Record = {}; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // Match YAML key: value pattern + const match = line.match(/^([A-Za-z0-9_\-]+):/); + if (!match) continue; + + const key = match[1]; + keyCounts[key] = (keyCounts[key] || 0) + 1; + + if (keyCounts[key] > 1) { + throw new Error(`Duplicate frontmatter key "${key}" found on line ${i + 1}.`); + } + } + } + } + const result = matter(content, { engines: parsers, ...format }); // in the absent of a body when serializing an entry we use an empty one // when calling `toFile`, so we don't want to add it when parsing. From db4c69b798db0e058d343c829f31e97265ce9dbb Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 8 Dec 2025 20:34:04 +0530 Subject: [PATCH 2/5] fix(frontmatter): improve duplicate key warning with file and line info --- packages/decap-cms-core/src/formats/frontmatter.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/decap-cms-core/src/formats/frontmatter.ts b/packages/decap-cms-core/src/formats/frontmatter.ts index 11b3f75a3a83..3c3fdbfa1388 100644 --- a/packages/decap-cms-core/src/formats/frontmatter.ts +++ b/packages/decap-cms-core/src/formats/frontmatter.ts @@ -98,7 +98,7 @@ export class FrontmatterFormatter { this.format = getFormatOpts(format, customDelimiter); } - fromFile(content: string) { + fromFile(content: string, filePath?: string) { const format = this.format || inferFrontmatterFormat(content); // Duplicate key detection for yaml frontmatter @@ -118,8 +118,10 @@ export class FrontmatterFormatter { const key = match[1]; keyCounts[key] = (keyCounts[key] || 0) + 1; + const source = filePath ?? 'unknown file'; + if (keyCounts[key] > 1) { - throw new Error(`Duplicate frontmatter key "${key}" found on line ${i + 1}.`); + console.warn(`Duplicate frontmatter key "${key}" in ${source} at line ${i + 1}`); } } } From a465a3bf2b2238273d05e30dca07e983e6ca3402 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 14 Dec 2025 20:15:50 +0530 Subject: [PATCH 3/5] fix(frontmatter): made duplicate detection path awarness using indendation --- .../decap-cms-core/src/formats/frontmatter.ts | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/packages/decap-cms-core/src/formats/frontmatter.ts b/packages/decap-cms-core/src/formats/frontmatter.ts index 3c3fdbfa1388..c43533341bf8 100644 --- a/packages/decap-cms-core/src/formats/frontmatter.ts +++ b/packages/decap-cms-core/src/formats/frontmatter.ts @@ -102,27 +102,43 @@ export class FrontmatterFormatter { const format = this.format || inferFrontmatterFormat(content); // Duplicate key detection for yaml frontmatter + { - const lines = content.split('\n'); - // Detect duplicate keys in frontmatter (YAML only) if (!this.format || this.format.language === 'yaml') { - const keyCounts: Record = {}; + const lines = content.split('\n'); + const seenPaths = new Set(); + const pathStack: { indent: number; key: string }[] = []; for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); + const rawLine = lines[i]; + const line = rawLine.trim(); + + // Skip empty lines and comments + if (!line || line.startsWith('#')) continue; - // Match YAML key: value pattern - const match = line.match(/^([A-Za-z0-9_\-]+):/); + const match = rawLine.match(/^(\s*)([A-Za-z0-9_\-]+):/); if (!match) continue; - const key = match[1]; - keyCounts[key] = (keyCounts[key] || 0) + 1; + const indent = match[1].length; + const key = match[2]; + + // Pop stack until current indent level is valid + while (pathStack.length > 0 && pathStack[pathStack.length - 1].indent >= indent) { + pathStack.pop(); + } + + const fullPath = [...pathStack.map(p => p.key), key].join('.'); const source = filePath ?? 'unknown file'; - if (keyCounts[key] > 1) { - console.warn(`Duplicate frontmatter key "${key}" in ${source} at line ${i + 1}`); + if (seenPaths.has(fullPath)) { + console.warn(`Duplicate frontmatter key "${fullPath}" in ${source} at line ${i + 1}`); + } else { + seenPaths.add(fullPath); } + + // Push current key for nested children + pathStack.push({ indent, key }); } } } From 5327cc3b7a7ffc7b6fd48f7b0f2c2e609df3c8f8 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 15 Dec 2025 14:39:22 +0530 Subject: [PATCH 4/5] fix(frontmatter): removed backslash --- packages/decap-cms-core/src/formats/frontmatter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/decap-cms-core/src/formats/frontmatter.ts b/packages/decap-cms-core/src/formats/frontmatter.ts index c43533341bf8..d552bec7c1ff 100644 --- a/packages/decap-cms-core/src/formats/frontmatter.ts +++ b/packages/decap-cms-core/src/formats/frontmatter.ts @@ -116,7 +116,7 @@ export class FrontmatterFormatter { // Skip empty lines and comments if (!line || line.startsWith('#')) continue; - const match = rawLine.match(/^(\s*)([A-Za-z0-9_\-]+):/); + const match = rawLine.match(/^(\s*)([A-Za-z0-9_-]+):/); if (!match) continue; const indent = match[1].length; From eb359edfc93c549eed21ad04c260b021af8809e2 Mon Sep 17 00:00:00 2001 From: Yan <61414485+yanthomasdev@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:29:28 -0300 Subject: [PATCH 5/5] feat: use `yaml` diagnostics --- .../src/formats/__tests__/frontmatter.spec.js | 21 +++++++++ .../src/formats/__tests__/yaml.spec.js | 27 +++++++++++ .../decap-cms-core/src/formats/frontmatter.ts | 45 +------------------ packages/decap-cms-core/src/formats/yaml.ts | 17 ++++++- 4 files changed, 65 insertions(+), 45 deletions(-) diff --git a/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js b/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js index 2f14d7324c12..428284c448de 100644 --- a/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js +++ b/packages/decap-cms-core/src/formats/__tests__/frontmatter.spec.js @@ -72,6 +72,27 @@ describe('Frontmatter', () => { }); }); + it('should throw on duplicate frontmatter keys', () => { + expect(() => + FrontmatterInfer.fromFile('---\ntitle: Hello\ntitle: World\n---\nContent'), + ).toThrow(/Map keys must be unique/); + }); + + it('should throw on duplicate frontmatter keys with explicit YAML format', () => { + expect(() => + frontmatterYAML().fromFile('---\ntitle: Hello\ntitle: World\n---\nContent'), + ).toThrow(/Map keys must be unique/); + }); + + it('should not throw when body contains YAML-like patterns', () => { + expect( + FrontmatterInfer.fromFile('---\ntitle: Hello\n---\ntitle: this is not a duplicate'), + ).toEqual({ + title: 'Hello', + body: 'title: this is not a duplicate', + }); + }); + it('should stringify YAML with --- delimiters', () => { expect( FrontmatterInfer.toFile({ diff --git a/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js b/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js index bff71d7c4377..79f9edae67be 100644 --- a/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js +++ b/packages/decap-cms-core/src/formats/__tests__/yaml.spec.js @@ -85,6 +85,33 @@ describe('yaml', () => { time: '10:05', }); }); + + test('throws on duplicate keys', () => { + expect(() => yaml.fromFile('title: Hello\ntitle: World')).toThrow( + /Map keys must be unique; "title" is repeated/, + ); + }); + + test('throws on duplicate nested keys', () => { + expect(() => yaml.fromFile('nested:\n a: 1\n a: 2')).toThrow( + /Map keys must be unique; "a" is repeated/, + ); + }); + + test('does not throw when same key appears in different nested objects', () => { + expect(yaml.fromFile('obj1:\n name: foo\nobj2:\n name: bar')).toEqual({ + obj1: { name: 'foo' }, + obj2: { name: 'bar' }, + }); + }); + + test('logs warnings to console.warn', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + // Valid YAML should produce no warnings + yaml.fromFile('title: Hello'); + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + }); }); describe('toFile', () => { test('outputs valid yaml', () => { diff --git a/packages/decap-cms-core/src/formats/frontmatter.ts b/packages/decap-cms-core/src/formats/frontmatter.ts index d552bec7c1ff..814ede8e3d6b 100644 --- a/packages/decap-cms-core/src/formats/frontmatter.ts +++ b/packages/decap-cms-core/src/formats/frontmatter.ts @@ -98,51 +98,8 @@ export class FrontmatterFormatter { this.format = getFormatOpts(format, customDelimiter); } - fromFile(content: string, filePath?: string) { + fromFile(content: string) { const format = this.format || inferFrontmatterFormat(content); - - // Duplicate key detection for yaml frontmatter - - { - if (!this.format || this.format.language === 'yaml') { - const lines = content.split('\n'); - const seenPaths = new Set(); - const pathStack: { indent: number; key: string }[] = []; - - for (let i = 0; i < lines.length; i++) { - const rawLine = lines[i]; - const line = rawLine.trim(); - - // Skip empty lines and comments - if (!line || line.startsWith('#')) continue; - - const match = rawLine.match(/^(\s*)([A-Za-z0-9_-]+):/); - if (!match) continue; - - const indent = match[1].length; - const key = match[2]; - - // Pop stack until current indent level is valid - while (pathStack.length > 0 && pathStack[pathStack.length - 1].indent >= indent) { - pathStack.pop(); - } - - const fullPath = [...pathStack.map(p => p.key), key].join('.'); - - const source = filePath ?? 'unknown file'; - - if (seenPaths.has(fullPath)) { - console.warn(`Duplicate frontmatter key "${fullPath}" in ${source} at line ${i + 1}`); - } else { - seenPaths.add(fullPath); - } - - // Push current key for nested children - pathStack.push({ indent, key }); - } - } - } - const result = matter(content, { engines: parsers, ...format }); // in the absent of a body when serializing an entry we use an empty one // when calling `toFile`, so we don't want to add it when parsing. diff --git a/packages/decap-cms-core/src/formats/yaml.ts b/packages/decap-cms-core/src/formats/yaml.ts index 923945b16240..bd2f42fe71c6 100644 --- a/packages/decap-cms-core/src/formats/yaml.ts +++ b/packages/decap-cms-core/src/formats/yaml.ts @@ -41,7 +41,22 @@ export default { if (content && content.trim().endsWith('---')) { content = content.trim().slice(0, -3); } - return yaml.parse(content, { customTags: [timestampTag] }); + + const doc = yaml.parseDocument(content, { + customTags: [timestampTag], + prettyErrors: true, + }); + + for (const warn of doc.warnings) { + console.warn(`YAML warning: ${warn.message}`); + } + + if (doc.errors.length > 0) { + const messages = doc.errors.map(e => e.message).join('\n'); + throw new Error(`YAML parsing error:\n${messages}`); + } + + return doc.toJSON(); }, toFile(data: object, sortedKeys: string[] = [], comments: Record = {}) {