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/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 = {}) {