diff --git a/package-lock.json b/package-lock.json index 741a1b1f2efa..ff363cfcc6c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", "js-cookie": "^3.0.7", - "js-yaml": "^4.2.0", + "js-yaml": "^5.1.0", "liquidjs": "^10.27.0", "lodash": "^4.18.0", "lodash-es": "^4.18.0", @@ -1176,6 +1176,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1358,6 +1381,28 @@ "node": ">=6.0" } }, + "node_modules/@gr2m/gray-matter/node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@graphql-inspector/core": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@graphql-inspector/core/-/core-7.1.2.tgz", @@ -11811,9 +11856,9 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.1.0.tgz", + "integrity": "sha512-s8VA5jkR8f22S3NAXmhKPFqGUduqZGlsufabVOgN14iTdw/RXcym7bKkbwjxLK9Yw2lEvvmJjFp119+KPeo8Kg==", "funding": [ { "type": "github", @@ -11829,7 +11874,7 @@ "argparse": "^2.0.1" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/json-buffer": { diff --git a/package.json b/package.json index f23bdff74747..e388efd04537 100644 --- a/package.json +++ b/package.json @@ -228,7 +228,7 @@ "is-svg": "6.0.0", "javascript-stringify": "^2.1.0", "js-cookie": "^3.0.7", - "js-yaml": "^4.2.0", + "js-yaml": "^5.1.0", "liquidjs": "^10.27.0", "lodash": "^4.18.0", "lodash-es": "^4.18.0", diff --git a/src/data-directory/lib/data-directory.ts b/src/data-directory/lib/data-directory.ts index b2ff0011a557..6098eb0e7f60 100644 --- a/src/data-directory/lib/data-directory.ts +++ b/src/data-directory/lib/data-directory.ts @@ -2,7 +2,7 @@ import assert from 'assert' import fs from 'fs' import path from 'path' import walk from 'walk-sync' -import { load } from 'js-yaml' +import { loadYaml } from '@/frame/lib/load-yaml' import { isRegExp, setWith } from 'lodash-es' import filenameToKey from './filename-to-key' import matter from '@gr2m/gray-matter' @@ -74,7 +74,7 @@ export default function dataDirectory( setWith(data, key, JSON.parse(processedContent), Object) break case '.yml': - setWith(data, key, load(processedContent, { filename }), Object) + setWith(data, key, loadYaml(processedContent, { filename }), Object) break case '.md': case '.markdown': diff --git a/src/data-directory/lib/get-data.ts b/src/data-directory/lib/get-data.ts index a9c1aedc252e..c585c9c25875 100644 --- a/src/data-directory/lib/get-data.ts +++ b/src/data-directory/lib/get-data.ts @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' -import { load } from 'js-yaml' +import { loadYaml } from '@/frame/lib/load-yaml' import matter from '@gr2m/gray-matter' import { merge, get } from 'lodash-es' @@ -331,7 +331,7 @@ const getYamlContent = memoize( root = englishRoot } const fileContent = getFileContent(root, relPath, englishRoot) - return load(fileContent, { filename: relPath }) + return loadYaml(fileContent, { filename: relPath }) }, ) diff --git a/src/frame/lib/load-yaml.ts b/src/frame/lib/load-yaml.ts new file mode 100644 index 000000000000..44f17e0f11e9 --- /dev/null +++ b/src/frame/lib/load-yaml.ts @@ -0,0 +1,25 @@ +import { load, loadAll, type LoadOptions } from 'js-yaml' + +// js-yaml v5's `load()` throws a `YAMLException` ("expected a document, but +// the input is empty") when the input contains no YAML document — i.e. when +// it is empty, whitespace-only, or comments-only. js-yaml v4 returned +// `undefined` in those cases. This wrapper restores the v4 behavior so callers +// that read arbitrary (possibly empty) YAML files keep working across the +// major version bump. +// +// For any input that does contain a document, this behaves exactly like +// `load()` — including throwing on genuinely malformed YAML. +export function loadYaml(content: string, options?: LoadOptions): unknown { + // `loadAll` invokes the iterator once per document and, unlike `load`, does + // not throw when there are zero documents. Collect the documents so we can + // reproduce `load`'s single-document semantics without parsing twice. + const documents: unknown[] = [] + loadAll(content, (doc) => documents.push(doc), options) + // No document (empty / whitespace-only / comments-only): v4 returned + // `undefined`. + if (documents.length === 0) return undefined + // Multiple documents: defer to `load` so it throws the same "expected a + // single document" error that v4 did. + if (documents.length > 1) return load(content, options) + return documents[0] +} diff --git a/src/frame/tests/load-yaml.ts b/src/frame/tests/load-yaml.ts new file mode 100644 index 000000000000..2a26c1a9b7a0 --- /dev/null +++ b/src/frame/tests/load-yaml.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from 'vitest' + +import { loadYaml } from '@/frame/lib/load-yaml' + +describe('loadYaml', () => { + test('returns undefined for empty or document-less content', () => { + expect(loadYaml('')).toBeUndefined() + expect(loadYaml(' \n\t')).toBeUndefined() + expect(loadYaml('# comment only\n# another comment')).toBeUndefined() + }) + + test('throws on multiple YAML documents', () => { + expect(() => loadYaml('a: 1\n---\nb: 2\n')).toThrow() + }) +}) diff --git a/src/links/lib/update-internal-links.ts b/src/links/lib/update-internal-links.ts index 665fd2111bea..96c85e48aa2c 100644 --- a/src/links/lib/update-internal-links.ts +++ b/src/links/lib/update-internal-links.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { visit, Test } from 'unist-util-visit' import { fromMarkdown } from 'mdast-util-from-markdown' import { toMarkdown } from 'mdast-util-to-markdown' -import { load } from 'js-yaml' +import { loadYaml } from '@/frame/lib/load-yaml' import { type Node, type Nodes, type Definition, type Link } from 'mdast' import type { Context, Page } from '@/types' @@ -101,7 +101,7 @@ async function updateFile(file: string, context: LinkContext, opts: typeof Optio // And since the Yaml file might contain arrays of internal linked // pathnames, we have to re-read it fully. if (file.endsWith('.yml')) { - Object.assign(data, load(content)) + Object.assign(data, loadYaml(content)) } let newContent = content diff --git a/src/workflows/generate-llms-txt.ts b/src/workflows/generate-llms-txt.ts index 70ecdb739839..30e4971e2c40 100644 --- a/src/workflows/generate-llms-txt.ts +++ b/src/workflows/generate-llms-txt.ts @@ -13,7 +13,7 @@ import fs from 'fs' -import { load } from 'js-yaml' +import { loadYaml } from '@/frame/lib/load-yaml' import { loadPageMap } from '@/frame/lib/page-data' import { renderContent } from '@/content-render/index' @@ -44,11 +44,11 @@ export const ROLLUP_URL = const BASE_URL = 'https://docs.github.com' export function loadConfig(overridePath?: string): LlmsTxtConfig { - const defaults = load(fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf8')) as Partial + const defaults = loadYaml(fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf8')) as Partial if (!overridePath || overridePath === DEFAULT_CONFIG_PATH) { return defaults as LlmsTxtConfig } - const overrides = load(fs.readFileSync(overridePath, 'utf8')) as Partial | null + const overrides = loadYaml(fs.readFileSync(overridePath, 'utf8')) as Partial | null return { ...(defaults as LlmsTxtConfig), ...(overrides || {}) } }