From 4bf0659a3bf9faf53781b60d6d46422e8a0f0b93 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Thu, 30 Oct 2025 11:44:17 +0100 Subject: [PATCH 1/6] fix(node): cloning Signed-off-by: skjnldsv --- lib/node/file.ts | 2 +- lib/node/folder.ts | 7 +++---- lib/node/node.ts | 16 ++++++---------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/node/file.ts b/lib/node/file.ts index ed6c5085c..c1e2966e5 100644 --- a/lib/node/file.ts +++ b/lib/node/file.ts @@ -15,7 +15,7 @@ export class File extends Node { * Returns a clone of the file */ clone(): File { - return new File(this.data) + return new File(this._data, this._knownDavService) } } diff --git a/lib/node/folder.ts b/lib/node/folder.ts index 05d0291d0..ef9af8f71 100644 --- a/lib/node/folder.ts +++ b/lib/node/folder.ts @@ -2,18 +2,17 @@ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { NodeData } from './nodeData' import { FileType } from './fileType' import { Node } from './node' export class Folder extends Node { - constructor(data: NodeData) { + constructor(...[data, davService]: ConstructorParameters) { // enforcing mimes super({ ...data, mime: 'httpd/unix-directory', - }) + }, davService) } get type(): FileType.Folder { @@ -32,7 +31,7 @@ export class Folder extends Node { * Returns a clone of the folder */ clone(): Folder { - return new Folder(this.data) + return new Folder(this._data, this._knownDavService) } } diff --git a/lib/node/node.ts b/lib/node/node.ts index 12e2b3831..089219f58 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -21,11 +21,14 @@ export enum NodeStatus { LOCKED = 'locked', } +type NodeConstructorData = [NodeData, RegExp?] + export abstract class Node { - private _data: NodeData private _attributes: Attribute - private _knownDavService = /(remote|public)\.php\/(web)?dav/i + + protected _data: NodeData + protected _knownDavService = /(remote|public)\.php\/(web)?dav/i private readonlyAttributes = Object.entries(Object.getOwnPropertyDescriptors(Node.prototype)) .filter(e => typeof e[1].get === 'function' && e[0] !== '__proto__') @@ -58,7 +61,7 @@ export abstract class Node { }, } as ProxyHandler - constructor(data: NodeData, davService?: RegExp) { + constructor(...[data, davService]: NodeConstructorData) { if (!data.mime) { data.mime = 'application/octet-stream' } @@ -346,13 +349,6 @@ export abstract class Node { this._data.status = status } - /** - * Get the node data - */ - get data(): NodeData { - return structuredClone(this._data) - } - /** * Move the node to a new destination * From b7ce2febe3a32901f424c188f2b227e73238f680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= Date: Tue, 4 Nov 2025 09:57:47 +0100 Subject: [PATCH 2/6] fix(node): use structuredClone to clone data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: John Molakvoæ --- lib/node/file.ts | 2 +- lib/node/folder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/node/file.ts b/lib/node/file.ts index c1e2966e5..6f495f7a5 100644 --- a/lib/node/file.ts +++ b/lib/node/file.ts @@ -15,7 +15,7 @@ export class File extends Node { * Returns a clone of the file */ clone(): File { - return new File(this._data, this._knownDavService) + return new File(structuredClone(this._data), this._knownDavService) } } diff --git a/lib/node/folder.ts b/lib/node/folder.ts index ef9af8f71..77066d338 100644 --- a/lib/node/folder.ts +++ b/lib/node/folder.ts @@ -31,7 +31,7 @@ export class Folder extends Node { * Returns a clone of the folder */ clone(): Folder { - return new Folder(this._data, this._knownDavService) + return new Folder(structuredClone(this._data), this._knownDavService) } } From a9f461e73b9f6e9ef0567f010ff94c79293e5739 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Thu, 30 Oct 2025 12:04:11 +0100 Subject: [PATCH 3/6] feat(node): allow to cast as String Signed-off-by: skjnldsv --- lib/node/node.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/node/node.ts b/lib/node/node.ts index 089219f58..6224758d6 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -422,6 +422,14 @@ export abstract class Node { */ abstract clone(): Node + /** + * String representation of the node + */ + toString(): string { + const constructorData: NodeConstructorData = [this._data, this._knownDavService] + return JSON.stringify(constructorData) + } + } /** From 563fcf241f879fc5d0c9b8be0433101c74348b72 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Thu, 30 Oct 2025 12:13:46 +0100 Subject: [PATCH 4/6] chore: add cloning tests Signed-off-by: skjnldsv --- __tests__/files/cloning.spec.ts | 307 ++++++++++++++++++++++++++++++++ lib/node/node.ts | 11 +- lib/node/nodeData.ts | 29 +++ package-lock.json | 7 + package.json | 1 + 5 files changed, 351 insertions(+), 4 deletions(-) create mode 100644 __tests__/files/cloning.spec.ts diff --git a/__tests__/files/cloning.spec.ts b/__tests__/files/cloning.spec.ts new file mode 100644 index 000000000..a4ad301cf --- /dev/null +++ b/__tests__/files/cloning.spec.ts @@ -0,0 +1,307 @@ +/* + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { describe, expect, test } from 'vitest' +import { File, NodeData, NodeStatus } from '../../lib/node/index.ts' +import { Permission } from '../../lib/permissions.ts' + +describe('File cloning', () => { + test('Clone preserves all basic properties', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)), + crtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), + size: 12345, + permissions: Permission.ALL, + root: '/files/emma', + status: NodeStatus.NEW, + }) + + const clone = file.clone() + + expect(clone).toBeInstanceOf(File) + expect(clone).not.toBe(file) + expect(clone.source).toBe(file.source) + expect(clone.mime).toBe(file.mime) + expect(clone.owner).toBe(file.owner) + expect(clone.size).toBe(file.size) + expect(clone.permissions).toBe(file.permissions) + expect(clone.root).toBe(file.root) + expect(clone.status).toBe(file.status) + expect(clone.mtime?.toISOString()).toBe(file.mtime?.toISOString()) + expect(clone.crtime?.toISOString()).toBe(file.crtime?.toISOString()) + }) + + test('Clone preserves attributes', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + attributes: { + favorite: true, + customProp: 'value', + nested: { key: 'value' }, + }, + }) + + const clone = file.clone() + + expect(clone.attributes).toStrictEqual(file.attributes) + expect(clone.attributes.favorite).toBe(true) + expect(clone.attributes.customProp).toBe('value') + expect(clone.attributes.nested).toStrictEqual({ key: 'value' }) + }) + + test('Clone is independent from original', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + size: 100, + attributes: { + test: 'original', + }, + }) + + const clone = file.clone() + + // Modify the clone + clone.size = 200 + clone.mime = 'image/png' + clone.attributes.test = 'modified' + clone.attributes.newProp = 'new' + + // Original should be unchanged + expect(file.size).toBe(100) + expect(file.mime).toBe('image/jpeg') + expect(file.attributes.test).toBe('original') + expect(file.attributes.newProp).toBeUndefined() + }) + + test('Clone works with minimal file', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/file.txt', + owner: 'emma', + }) + + const clone = file.clone() + + expect(clone).toBeInstanceOf(File) + expect(clone.source).toBe(file.source) + expect(clone.mime).toBe('application/octet-stream') + expect(clone.owner).toBe('emma') + }) + + test('Clone works with remote file', () => { + const file = new File({ + source: 'https://domain.com/Photos/picture.jpg', + mime: 'image/jpeg', + owner: null, + }) + + const clone = file.clone() + + expect(clone).toBeInstanceOf(File) + expect(clone.source).toBe(file.source) + expect(clone.owner).toBeNull() + expect(clone.isDavResource).toBe(false) + expect(clone.permissions).toBe(Permission.READ) + }) +}) + +describe('File serialization and deserialization', () => { + test('toString and JSON.parse roundtrip preserves all properties', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)), + crtime: new Date(Date.UTC(1990, 0, 1, 0, 0, 0)), + size: 12345, + permissions: Permission.ALL, + root: '/files/emma', + status: NodeStatus.LOADING, + }) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + expect(reconstructed).toBeInstanceOf(File) + expect(reconstructed.source).toBe(file.source) + expect(reconstructed.mime).toBe(file.mime) + expect(reconstructed.owner).toBe(file.owner) + expect(reconstructed.size).toBe(file.size) + expect(reconstructed.permissions).toBe(file.permissions) + expect(reconstructed.root).toBe(file.root) + expect(reconstructed.status).toBe(file.status) + expect(reconstructed.mtime?.toISOString()).toBe(file.mtime?.toISOString()) + expect(reconstructed.crtime?.toISOString()).toBe(file.crtime?.toISOString()) + }) + + test('toString and JSON.parse preserves attributes', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + attributes: { + favorite: true, + tags: ['work', 'important'], + metadata: { author: 'Emma', version: 2 }, + }, + }) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + expect(reconstructed.attributes).toStrictEqual(file.attributes) + expect(reconstructed.attributes.favorite).toBe(true) + expect(reconstructed.attributes.tags).toStrictEqual(['work', 'important']) + expect(reconstructed.attributes.metadata).toStrictEqual({ author: 'Emma', version: 2 }) + }) + + test('toString and JSON.parse works with minimal file', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/file.txt', + owner: 'emma', + }) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + expect(reconstructed).toBeInstanceOf(File) + expect(reconstructed.source).toBe(file.source) + expect(reconstructed.mime).toBe('application/octet-stream') + expect(reconstructed.owner).toBe('emma') + }) + + test('toString and JSON.parse is independent from original', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + size: 100, + attributes: { + test: 'original', + }, + }) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + // Modify the reconstructed file + reconstructed.size = 200 + reconstructed.mime = 'image/png' + reconstructed.attributes.test = 'modified' + + // Original should be unchanged + expect(file.size).toBe(100) + expect(file.mime).toBe('image/jpeg') + expect(file.attributes.test).toBe('original') + }) + + test('toString and JSON.parse preserves displayname', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + attributes: { + displayname: 'My Vacation Photo', + }, + }) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + expect(reconstructed.displayname).toBe('My Vacation Photo') + expect(reconstructed.basename).toBe('picture.jpg') + }) + + test('toString and JSON.parse works with remote file', () => { + const file = new File({ + source: 'https://domain.com/Photos/picture.jpg', + mime: 'image/jpeg', + owner: null, + }) + + expect(file.owner).toBeNull() + expect(file.isDavResource).toBe(false) + expect(file.permissions).toBe(Permission.READ) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + expect(reconstructed).toBeInstanceOf(File) + expect(reconstructed.source).toBe(file.source) + expect(reconstructed.owner).toBeNull() + expect(reconstructed.isDavResource).toBe(false) + expect(reconstructed.permissions).toBe(Permission.READ) + }) + + test('toString and JSON.parse preserves all NodeStatus values', () => { + const statuses = [NodeStatus.NEW, NodeStatus.FAILED, NodeStatus.LOADING, NodeStatus.LOCKED] + + for (const status of statuses) { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/file.txt', + owner: 'emma', + status, + }) + + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + expect(reconstructed.status).toBe(status) + } + }) + + test('toString output is valid JSON', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + size: 12345, + }) + + const str = file.toString() + expect(() => JSON.parse(str)).not.toThrow() + + const parsed = JSON.parse(str) + expect(Array.isArray(parsed)).toBe(true) + expect(parsed.length).toBeGreaterThanOrEqual(1) + expect(parsed[0]).toHaveProperty('source') + }) +}) + +describe('Cloning methods comparison', () => { + test('clone() and toString/parse produce equivalent files', () => { + const file = new File({ + source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', + mime: 'image/jpeg', + owner: 'emma', + mtime: new Date(Date.UTC(2023, 0, 1, 0, 0, 0)), + size: 12345, + permissions: Permission.ALL, + root: '/files/emma', + attributes: { + favorite: true, + tags: ['work'], + }, + }) + + const cloned = file.clone() + const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const reconstructed = new File(parsed[0], parsed[1]) + + expect(cloned.source).toBe(reconstructed.source) + expect(cloned.mime).toBe(reconstructed.mime) + expect(cloned.owner).toBe(reconstructed.owner) + expect(cloned.size).toBe(reconstructed.size) + expect(cloned.permissions).toBe(reconstructed.permissions) + expect(cloned.root).toBe(reconstructed.root) + expect(cloned.mtime?.toISOString()).toBe(reconstructed.mtime?.toISOString()) + expect(cloned.attributes).toStrictEqual(reconstructed.attributes) + }) +}) diff --git a/lib/node/node.ts b/lib/node/node.ts index 6224758d6..044cd413a 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -7,7 +7,7 @@ import { encodePath } from '@nextcloud/paths' import { Permission } from '../permissions' import { FileType } from './fileType' -import { Attribute, isDavResource, NodeData, validateData } from './nodeData' +import { Attribute, fixDates, fixRegExp, isDavResource, NodeData, validateData } from './nodeData' import logger from '../utils/logger' export enum NodeStatus { @@ -66,8 +66,12 @@ export abstract class Node { data.mime = 'application/octet-stream' } + // Fix primitive types if needed + fixDates(data) + davService = fixRegExp(davService || this._knownDavService) + // Validate data - validateData(data, davService || this._knownDavService) + validateData(data, davService) this._data = { // TODO: Remove with next major release, this is just for compatibility @@ -426,8 +430,7 @@ export abstract class Node { * String representation of the node */ toString(): string { - const constructorData: NodeConstructorData = [this._data, this._knownDavService] - return JSON.stringify(constructorData) + return JSON.stringify([this._data, this._knownDavService.toString()]) } } diff --git a/lib/node/nodeData.ts b/lib/node/nodeData.ts index c387ba202..4508de81c 100644 --- a/lib/node/nodeData.ts +++ b/lib/node/nodeData.ts @@ -4,6 +4,8 @@ */ import { join } from 'path' +import RegexParser from 'regex-parser' + import { Permission } from '../permissions' import { NodeStatus } from './node' @@ -157,3 +159,30 @@ export const validateData = (data: NodeData, davService: RegExp) => { throw new Error('Status must be a valid NodeStatus') } } + +/** + * In case we try to create a node from deserialized data, + * we need to fix date types. + */ +export const fixDates = (data: NodeData) => { + if (data.mtime && typeof data.mtime === 'string') { + if (!isNaN(Date.parse(data.mtime)) + && JSON.stringify(new Date(data.mtime)) === JSON.stringify(data.mtime)) { + data.mtime = new Date(data.mtime) + } + } + + if (data.crtime && typeof data.crtime === 'string') { + if (!isNaN(Date.parse(data.crtime)) + && JSON.stringify(new Date(data.crtime)) === JSON.stringify(data.crtime)) { + data.crtime = new Date(data.crtime) + } + } +} + +export const fixRegExp = (pattern: string | RegExp): RegExp => { + if (typeof pattern === 'string') { + return RegexParser(pattern) + } + return pattern +} diff --git a/package-lock.json b/package-lock.json index 8a94bb368..5c792e6cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nextcloud/sharing": "^0.3.0", "cancelable-promise": "^4.3.1", "is-svg": "^6.1.0", + "regex-parser": "^2.3.1", "typescript-event-target": "^1.1.1", "webdav": "^5.8.0" }, @@ -8362,6 +8363,12 @@ "node": ">=8.10.0" } }, + "node_modules/regex-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", + "license": "MIT" + }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", diff --git a/package.json b/package.json index d384bce39..d9c93a69c 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@nextcloud/sharing": "^0.3.0", "cancelable-promise": "^4.3.1", "is-svg": "^6.1.0", + "regex-parser": "^2.3.1", "typescript-event-target": "^1.1.1", "webdav": "^5.8.0" }, From 78e979ddcee3836e2bda3caf3550599e53671c81 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Tue, 4 Nov 2025 10:04:52 +0100 Subject: [PATCH 5/6] fix(node): use toJSON Signed-off-by: skjnldsv --- __tests__/files/cloning.spec.ts | 20 ++++++++++---------- lib/node/node.ts | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/__tests__/files/cloning.spec.ts b/__tests__/files/cloning.spec.ts index a4ad301cf..7b3c8156d 100644 --- a/__tests__/files/cloning.spec.ts +++ b/__tests__/files/cloning.spec.ts @@ -114,7 +114,7 @@ describe('File cloning', () => { }) describe('File serialization and deserialization', () => { - test('toString and JSON.parse roundtrip preserves all properties', () => { + test('toJSON and JSON.parse roundtrip preserves all properties', () => { const file = new File({ source: 'https://cloud.domain.com/remote.php/dav/files/emma/Photos/picture.jpg', mime: 'image/jpeg', @@ -127,7 +127,7 @@ describe('File serialization and deserialization', () => { status: NodeStatus.LOADING, }) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(reconstructed).toBeInstanceOf(File) @@ -154,7 +154,7 @@ describe('File serialization and deserialization', () => { }, }) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(reconstructed.attributes).toStrictEqual(file.attributes) @@ -169,7 +169,7 @@ describe('File serialization and deserialization', () => { owner: 'emma', }) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(reconstructed).toBeInstanceOf(File) @@ -189,7 +189,7 @@ describe('File serialization and deserialization', () => { }, }) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) // Modify the reconstructed file @@ -213,7 +213,7 @@ describe('File serialization and deserialization', () => { }, }) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(reconstructed.displayname).toBe('My Vacation Photo') @@ -231,7 +231,7 @@ describe('File serialization and deserialization', () => { expect(file.isDavResource).toBe(false) expect(file.permissions).toBe(Permission.READ) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(reconstructed).toBeInstanceOf(File) @@ -251,7 +251,7 @@ describe('File serialization and deserialization', () => { status, }) - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(reconstructed.status).toBe(status) } @@ -265,7 +265,7 @@ describe('File serialization and deserialization', () => { size: 12345, }) - const str = file.toString() + const str = file.toJSON() expect(() => JSON.parse(str)).not.toThrow() const parsed = JSON.parse(str) @@ -292,7 +292,7 @@ describe('Cloning methods comparison', () => { }) const cloned = file.clone() - const parsed = JSON.parse(file.toString()) as [NodeData, RegExp?] + const parsed = JSON.parse(file.toJSON()) as [NodeData, RegExp?] const reconstructed = new File(parsed[0], parsed[1]) expect(cloned.source).toBe(reconstructed.source) diff --git a/lib/node/node.ts b/lib/node/node.ts index 044cd413a..68d596c24 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -427,10 +427,10 @@ export abstract class Node { abstract clone(): Node /** - * String representation of the node + * JSON representation of the node */ - toString(): string { - return JSON.stringify([this._data, this._knownDavService.toString()]) + toJSON(): string { + return JSON.stringify([structuredClone(this._data), this._knownDavService.toString()]) } } From 3d61d885207bf335b54e3678c8208820507943f2 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Tue, 4 Nov 2025 10:07:48 +0100 Subject: [PATCH 6/6] chore: allow npm 11 Signed-off-by: skjnldsv --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d9c93a69c..7b37ead02 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "packageManager": [ { "name": "npm", - "version": "^10.0.0" + "version": "^10.0.0 || ^11.0.0" } ], "runtime": {