// vibed import { deepStrictEqual, equal, ok, strictEqual, throws, } from "node:assert/strict"; import { Document, FjsonError, parse } from "./mod.ts"; Deno.test("replacing a leaf preserves every other byte and quote style", () => { const source = "// head\r\nimports: { 'de\\u0070': /* choice */ 'old' /* reason */ },\r\nother: 1e3\r\n"; const document = Document.parse(source); document.set(["imports", "dep"], "it's new\nšŸ˜€"); strictEqual( document.source, source.replace("'old'", "'it\\'s new\\nšŸ˜€'"), ); deepStrictEqual(parse(document.source), { imports: { dep: "it's new\nšŸ˜€" }, other: 1000, }); }); Deno.test("unchanged values do not reformat or remove internal comments", () => { const source = "items: [1, /* why */ 2,]\nname: 'same'\n"; const document = Document.parse(source); document.set(["items"], [1, 2]); document.set(["name"], "same"); strictEqual(document.source, source); }); Deno.test("insertion preserves comments, line endings, and existing layout", () => { for (const newline of ["\n", "\r\n", "\r"]) { const source = `// head${newline}options: {${newline}\told: 'kept' // reason${newline}}${newline}`; const document = Document.parse(source); document.set(["options", "new-key"], { enabled: true }); ok(document.source.includes(`\told: 'kept' // reason${newline}`)); ok( document.source.includes( `\t"new-key": {${newline}\t enabled: true${newline}\t}`, ), ); deepStrictEqual(parse(document.source), { options: { old: "kept", "new-key": { enabled: true } }, }); if (newline !== "\n") { ok(!document.source.replaceAll(newline, "").includes("\n")); } } for ( const source of [ "", "// eof", "{/* empty */}", "/* heading */\n", "{\n // empty\n}", ] ) { const document = Document.parse(source); document.set(["ę—„ęœ¬čŖž"], [true, false]); deepStrictEqual(parse(document.source), { "ę—„ęœ¬čŖž": [true, false] }); for ( const comment of ["// eof", "/* empty */", "/* heading */", "// empty"] ) { if (source.includes(comment)) { ok(document.source.includes(comment), comment); } } } }); Deno.test("array appends and sequential edits use updated paths", () => { const document = Document.parse( "list: [/* first */ 1, // note\n 2,]\nother: 'keep'", ); document.set(["list", 2], { name: "new" }); document.remove(["list", 0]); document.set(["list", 1, "name"], "updated"); deepStrictEqual(parse(document.source), { list: [2, { name: "updated" }], other: "keep", }); for (const kept of ["/* first */", "// note", "other: 'keep'"]) { ok(document.source.includes(kept), kept); } }); Deno.test("removing a member keeps external comments but deletes its contents", () => { const source = "// head\na /* key : šŸ˜€ */ : /* before */ { nested: [/* inside */ 1] } /* after , */ , // line\nb: 'kept'"; const document = Document.parse(source); strictEqual(document.remove(["a"]), true); deepStrictEqual(parse(document.source), { b: "kept" }); for ( const comment of [ "// head", "/* key : šŸ˜€ */", "/* before */", "/* after , */", "// line", ] ) { ok(document.source.includes(comment), comment); } ok(!document.source.includes("/* inside */")); ok(document.source.endsWith("b: 'kept'")); strictEqual(document.remove(["a"]), false); }); Deno.test("removal handles all separator styles and positions", () => { for ( const separator of [ ",", ", ", "\n", " /* gap , šŸ˜€ */\n", ", // gap\n", "\r\n", ] ) { for (const trailing of ["", ",", "\n", ",\n"]) { for (const braced of [false, true]) { for (let index = 0; index < 3; index += 1) { const body = `a: 1${separator}b: 2${separator}c: 3${trailing}`; const document = Document.parse(braced ? `{${body}}` : body); const keys = ["a", "b", "c"]; strictEqual(document.remove([keys[index]]), true); const expected: Record = { a: 1, b: 2, c: 3 }; delete expected[keys[index]]; deepStrictEqual(parse(document.source), expected, document.source); for (const key of keys) document.remove([key]); deepStrictEqual(parse(document.source), {}); document.set(["new"], true); deepStrictEqual(parse(document.source), { new: true }); } } for (let index = 0; index < 3; index += 1) { const document = Document.parse( `[1${separator}2${separator}3${trailing}]`, ); document.remove([index]); const expected = [1, 2, 3]; expected.splice(index, 1); deepStrictEqual(parse(document.source), expected); document.remove([0]); document.remove([0]); deepStrictEqual(parse(document.source), []); document.set([0], "again"); deepStrictEqual(parse(document.source), ["again"]); } } } }); Deno.test("replacing a container removes only comments inside it", () => { const document = Document.parse( "keep: 1\nobject: /* before */ {x: 1 /* inside */} // after\n", ); document.set(["object"], false); strictEqual( document.source, "keep: 1\nobject: /* before */ false // after\n", ); const root = Document.parse("// head\n[/* inside */ 1] // tail"); root.set([], { new: true }); strictEqual(root.source, "// head\n{\n new: true\n} // tail"); }); Deno.test("root replacements preserve surrounding trivia", () => { for ( const source of [ "", " ", "// heading", " // heading", "/* heading */", "// heading\n", "// heading\n{} // tail", "// heading\n[1] // tail", "// heading\nfalse // tail", "// heading\nold: true", "// heading\nold: true // tail\n", "// heading\nold: true,\n// tail\n", ] ) { for (const value of [true, null, 42, "new", { new: true }, [], [1, 2]]) { const document = Document.parse(source); document.set([], value); deepStrictEqual(parse(document.source), value, document.source); for (const kept of ["// heading", "/* heading */", "// tail"]) { if (source.includes(kept)) ok(document.source.includes(kept), kept); } } } }); Deno.test("removing the last implicit member preserves external comments", () => { const source = "a /* key */ : /* before */ { x: 1 /* inside */ } /* after, */ , // tail\n"; const document = Document.parse(source); document.remove(["a"]); deepStrictEqual(parse(document.source), {}); for ( const comment of ["/* key */", "/* before */", "/* after, */", "// tail"] ) { ok(document.source.includes(comment), comment); } ok(!document.source.includes("/* inside */")); document.set(["b"], 2); deepStrictEqual(parse(document.source), { b: 2 }); }); Deno.test("object documents require object roots", () => { const error = (() => { try { Document.parseObject("imports {}"); } catch (caught) { ok(caught instanceof FjsonError); return caught; } throw new Error("expected error"); })(); strictEqual(error.render("config", "imports {}"), "config:1:9: Expected `:`"); for (const source of ["false", "1", "[]", "'a'"]) { throws(() => Document.parseObject(source), FjsonError, source); } const document = Document.parseObject("a: 1"); document.set([], 5); deepStrictEqual(document.toValue(), 5); }); Deno.test("invalid paths and over-limit edits are atomic", () => { const source = "items: [1]\nscalar: false"; const document = Document.parse(source); const bad: (string | number)[][] = [ ["missing", "child"], ["items", 2], ["items", "wrong"], ["scalar", "child"], [0], ]; for (const path of bad) { throws(() => document.set(path, true), FjsonError, JSON.stringify(path)); strictEqual(document.source, source); } throws(() => document.remove([]), FjsonError); strictEqual(document.source, source); let nested: unknown = null; for (let i = 0; i < 130; i += 1) nested = [nested]; const error = (() => { try { document.set(["items"], nested); } catch (caught) { ok(caught instanceof FjsonError); return caught; } throw new Error("expected nesting error"); })(); strictEqual(error.kind, "nesting"); error.render("test", document.source); strictEqual(document.source, source); }); Deno.test("documents remember their options for later edits", () => { const document = Document.parse("value: [[[0]]]", { maxDepth: 10 }); deepStrictEqual(document.options, { maxDepth: 10 }); let deep: unknown = null; for (let i = 0; i < 10; i += 1) deep = [deep]; document.set(["value"], deep); deepStrictEqual(document.toValue(), { value: deep }); throws(() => Document.parse("value: [[[0]]]", { maxDepth: 2 }), FjsonError); }); Deno.test("unserializable values throw TypeError", () => { const document = Document.parse("a: 1"); for (const value of [undefined, () => {}, Symbol("x"), 10n]) { throws(() => document.set(["a"], value), TypeError); } strictEqual(document.source, "a: 1"); }); Deno.test("toValue converts the document", () => { const document = Document.parse("a: [1, {b: 'x'}]"); deepStrictEqual(document.toValue(), { a: [1, { b: "x" }] }); strictEqual(document.root().kind, "object"); });