diff --git a/modules/standard-site-sync.ts b/modules/standard-site-sync.ts index 5ba317b2ef..f182489f39 100644 --- a/modules/standard-site-sync.ts +++ b/modules/standard-site-sync.ts @@ -1,12 +1,11 @@ import process from 'node:process' -import { readFileSync } from 'node:fs' import { createHash } from 'node:crypto' import { defineNuxtModule, useNuxt, createResolver } from 'nuxt/kit' import { safeParse } from 'valibot' import * as site from '../shared/types/lexicons/site' import { BlogPostSchema } from '../shared/schemas/blog' import { NPMX_SITE } from '../shared/utils/constants' -import { parseBasicFrontmatter } from '../shared/utils/parse-basic-frontmatter' +import { read } from 'gray-matter' import { TID } from '@atproto/common' import { Client } from '@atproto/lex' @@ -77,8 +76,7 @@ export default defineNuxtModule({ * WARN: DOES NOT CATCH ERRORS, THIS MUST BE HANDLED */ const syncFile = async (filePath: string, siteUrl: string, client: Client) => { - const fileContent = readFileSync(filePath, 'utf-8') - const frontmatter = parseBasicFrontmatter(fileContent) + const { data: frontmatter } = read(filePath) // Schema expects 'path' & frontmatter provides 'slug' const normalizedFrontmatter = { diff --git a/package.json b/package.json index d935fffc76..029870dd57 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "defu": "6.1.4", "fast-npm-meta": "1.0.0", "focus-trap": "^7.8.0", + "gray-matter": "4.0.3", "marked": "17.0.1", "module-replacements": "2.11.0", "nuxt": "4.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28a40bfe2f..0380d42f03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: focus-trap: specifier: ^7.8.0 version: 7.8.0 + gray-matter: + specifier: 4.0.3 + version: 4.0.3 marked: specifier: 17.0.1 version: 17.0.1 @@ -4688,6 +4691,9 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -5722,6 +5728,10 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -6043,6 +6053,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -6359,6 +6373,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -6596,6 +6614,10 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -6654,6 +6676,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -8303,6 +8329,10 @@ packages: scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -8490,6 +8520,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + srvx@0.10.1: resolution: {integrity: sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg==} engines: {node: '>=20.16.0'} @@ -8572,6 +8605,10 @@ packages: resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} @@ -14675,6 +14712,10 @@ snapshots: - bare-abort-controller - react-native-b4a + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -15931,6 +15972,10 @@ snapshots: exsolve@1.0.8: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extend@3.0.2: {} fake-indexeddb@6.2.5: {} @@ -16341,6 +16386,13 @@ snapshots: graceful-fs@4.2.11: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -16773,6 +16825,8 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -16987,6 +17041,11 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -17042,6 +17101,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + kleur@3.0.3: {} kleur@4.1.5: {} @@ -19641,6 +19702,11 @@ snapshots: scule@1.3.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.7.3: {} @@ -19891,6 +19957,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.0.3: {} + srvx@0.10.1: {} standard-as-callback@2.1.0: {} @@ -20006,6 +20074,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-comments@2.0.1: {} strip-final-newline@3.0.0: {} diff --git a/test/unit/shared/utils/parse-basic-frontmatter.spec.ts b/test/unit/shared/utils/parse-basic-frontmatter.spec.ts deleted file mode 100644 index 0b2d177d70..0000000000 --- a/test/unit/shared/utils/parse-basic-frontmatter.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { parseBasicFrontmatter } from '../../../../shared/utils/parse-basic-frontmatter' - -describe('parseBasicFrontmatter', () => { - it('returns empty object for content without frontmatter', () => { - expect(parseBasicFrontmatter('just some text')).toEqual({}) - }) - - it('returns empty object for empty string', () => { - expect(parseBasicFrontmatter('')).toEqual({}) - }) - - it('returns empty object for empty frontmatter block', () => { - expect(parseBasicFrontmatter('---\n---\n')).toEqual({}) - }) - - it('parses string values', () => { - const input = '---\ntitle: Hello World\nauthor: James\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - title: 'Hello World', - author: 'James', - }) - }) - - it('strips surrounding quotes from values', () => { - const input = '---\ntitle: "Hello World"\nauthor: \'James\'\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - title: 'Hello World', - author: 'James', - }) - }) - - it('parses boolean true', () => { - const input = '---\ndraft: true\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ draft: true }) - }) - - it('parses boolean false', () => { - const input = '---\ndraft: false\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ draft: false }) - }) - - it('parses integer values', () => { - const input = '---\ncount: 42\nnegative: -7\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ count: 42, negative: -7 }) - }) - - it('parses float values', () => { - const input = '---\nrating: 4.5\nnegative: -3.14\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ rating: 4.5, negative: -3.14 }) - }) - - it('parses array values', () => { - const input = '---\ntags: [foo, bar, baz]\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - tags: ['foo', 'bar', 'baz'], - }) - }) - - it('strips quotes from array items', () => { - const input = '---\ntags: ["foo", \'bar\']\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - tags: ['foo', 'bar'], - }) - }) - - it('does not support nested arrays', () => { - const input = '---\nmatrix: [[1, 2], [3, 4]]\n---\n' - const result = parseBasicFrontmatter(input) - expect(result.matrix).toEqual(['[1', '2]', '[3', '4]']) - }) - - it('handles values with colons', () => { - const input = '---\nurl: https://example.com\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - url: 'https://example.com', - }) - }) - - it('skips lines without colons', () => { - const input = '---\ntitle: Hello\ninvalid line\nauthor: James\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - title: 'Hello', - author: 'James', - }) - }) - - it('trims keys and values', () => { - const input = '---\n title : Hello \n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ title: 'Hello' }) - }) - - it('handles frontmatter at end of file without trailing newline', () => { - const input = '---\ntitle: Hello\n---' - expect(parseBasicFrontmatter(input)).toEqual({ title: 'Hello' }) - }) - - it('handles mixed types', () => { - const input = '---\ntitle: My Post\ncount: 5\nrating: 9.8\npublished: true\ntags: [a, b]\n---\n' - expect(parseBasicFrontmatter(input)).toEqual({ - title: 'My Post', - count: 5, - rating: 9.8, - published: true, - tags: ['a', 'b'], - }) - }) -})