diff --git a/packages/lib-common/package.json b/packages/lib-common/package.json index 42549c1381..395d2ed797 100644 --- a/packages/lib-common/package.json +++ b/packages/lib-common/package.json @@ -29,7 +29,7 @@ "@types/tinycolor2": "^1.4.6", "cross-spawn": "^7.0.6", "fast-check": "^4.8.0", - "js-yaml": "^4.2.0", + "js-yaml": "^5.2.1", "web-tree-sitter": "^0.26.10" } } diff --git a/packages/lib-common/src/testUtil/serialize.ts b/packages/lib-common/src/testUtil/serialize.ts index 870476879a..3342402833 100644 --- a/packages/lib-common/src/testUtil/serialize.ts +++ b/packages/lib-common/src/testUtil/serialize.ts @@ -1,70 +1,82 @@ -// From https://github.com/nodeca/js-yaml/issues/586#issuecomment-814310104 // This file ensures that simple objects and arrays (ie without array or object // children) will be serialized inline, and also ensures that "fullTargets" will be inlined as well -import { dump, Type, DEFAULT_SCHEMA } from "js-yaml"; -import type { DumpOptions } from "js-yaml"; +import type { Document, MappingNode, Node, SequenceNode } from "js-yaml"; +import { CORE_SCHEMA, dump, visit, YAML11_SCHEMA } from "js-yaml"; -class CustomDump { - constructor( - private readonly data: unknown, - private readonly opts: DumpOptions, - ) {} - - represent(): string { - let result = dump(this.data, { replacer, schema, ...this.opts }); - result = result.trim(); - if (result.includes("\n")) { - result = `\n${result}`; - } - return result; - } +export function serialize(obj: unknown): string { + return dump(toSerializableObject(obj), { + noRefs: true, + quoteStyle: "double", + // CORE_SCHEMA preserves existing fixture output for numeric-looking strings + // like `1_01_001`, which js-yaml's default schema would quote. + schema: CORE_SCHEMA, + transform: inlineSimpleCollections, + }); } -const customDumpType = new Type("!format", { - kind: "scalar", - resolve: () => false, - instanceOf: CustomDump, - represent: (d: unknown) => (d as CustomDump).represent(), -}); - -const schema = DEFAULT_SCHEMA.extend({ implicit: [customDumpType] }); - -const isObject = (value: unknown): value is object => - typeof value === "object" && value != null; - -function hasSimpleChildren(value: unknown): boolean { - if (isObject(value)) { - return Object.values(value).every( - (value) => !isObject(value) && !Array.isArray(value), - ); - } +function toSerializableObject(value: unknown): unknown { if (Array.isArray(value)) { - return value.every((value) => !isObject(value) && !Array.isArray(value)); + return value.map(toSerializableObject); } - return false; -} -function replacer(key: string, value: unknown): unknown { - // top-level, don't change this - if (key === "") { + if (value == null || typeof value !== "object") { return value; } - if (hasSimpleChildren(value)) { - return new CustomDump(value, { flowLevel: 0 }); - } + return Object.fromEntries( + Object.entries(value).map(([key, value]) => [ + key, + toSerializableObject(value), + ]), + ); +} + +function inlineSimpleCollections(documents: Document[]): void { + visit(documents, (node: Node, { depth, isKey, parent }) => { + // Keep nested simple objects and arrays in flow style, eg `{line: 0}` and + // `[default.a]`, while leaving top-level mappings in block style. + if (depth > 0 && isCollectionNode(node) && hasOnlyScalarChildren(node)) { + node.style.flow = true; + } - // default - return value; + // Existing fixtures use single quotes for flow scalar values that need + // quoting, eg `{character: ']'}`. Do not apply this to keys, non-string + // scalars, block scalars, or multiline strings. + if ( + !isKey && + parent != null && + parent.style.flow && + node.kind === "scalar" && + node.tag === "tag:yaml.org,2002:str" && + !node.value.includes("\n") && + scalarNeedsQuotesInFlow(node.value) + ) { + node.style.singleQuoted = true; + } + }); } -export function serialize(obj: unknown): string { - const dump = new CustomDump(obj, { - noRefs: true, - quotingType: '"', - }) - .represent() - .trim(); - return `${dump}\n`; +function isCollectionNode(node: Node): node is MappingNode | SequenceNode { + return node.kind === "mapping" || node.kind === "sequence"; +} + +function hasOnlyScalarChildren(node: MappingNode | SequenceNode): boolean { + return node.kind === "mapping" + ? node.items.every((item) => item.value.kind === "scalar") + : node.items.every((item) => item.kind === "scalar"); +} + +function scalarNeedsQuotesInFlow(value: string): boolean { + // Dump the value inside a flow array so js-yaml applies the same scalar + // rules it would use for an inline collection. If the result starts with + // `["`, the value cannot be emitted plainly in flow style, so we switch the + // actual fixture value to single quotes to match the existing fixtures. + return dump([value], { + flowLevel: 0, + quoteStyle: "double", + // Use YAML 1.1 schema to preserve existing fixtures where legacy + // boolean-like strings such as `y` and `n` are quoted in flow collections. + schema: YAML11_SCHEMA, + }).startsWith('["'); } diff --git a/packages/lib-node-common/package.json b/packages/lib-node-common/package.json index f8fb76609a..e42fca566a 100644 --- a/packages/lib-node-common/package.json +++ b/packages/lib-node-common/package.json @@ -21,6 +21,6 @@ "devDependencies": { "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", - "js-yaml": "^4.2.0" + "js-yaml": "^5.2.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 546b0fd0c9..bd374483ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -397,8 +397,8 @@ importers: specifier: ^4.8.0 version: 4.8.0 js-yaml: - specifier: ^4.2.0 - version: 4.3.0 + specifier: ^5.2.1 + version: 5.2.1 web-tree-sitter: specifier: ^0.26.10 version: 0.26.10 @@ -516,8 +516,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 js-yaml: - specifier: ^4.2.0 - version: 4.3.0 + specifier: ^5.2.1 + version: 5.2.1 packages/lib-sentence-parser: devDependencies: @@ -7743,6 +7743,10 @@ packages: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true + js-yaml@5.2.1: + resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} + hasBin: true + jsdom@26.1.0: resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} engines: {node: '>=18'} @@ -20059,6 +20063,10 @@ snapshots: dependencies: argparse: 2.0.1 + js-yaml@5.2.1: + dependencies: + argparse: 2.0.1 + jsdom@26.1.0: dependencies: cssstyle: 4.6.0