From ef6f0c7ffa2636c19e61932c4e8195fa01fde2c3 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Mon, 26 Jun 2023 20:21:29 +0300
Subject: [PATCH] feat: Use `klona` library for deep clone (#22979)

---
 .../cdnjs/__snapshots__/index.spec.ts.snap    |  1 +
 .../__snapshots__/index.spec.ts.snap          |  1 +
 lib/modules/datasource/index.ts               |  2 +-
 .../npm/__snapshots__/index.spec.ts.snap      | 72 +++++++++++++++++++
 lib/util/clone.spec.ts                        | 27 ++++---
 lib/util/clone.ts                             | 18 ++++-
 package.json                                  |  1 +
 7 files changed, 110 insertions(+), 12 deletions(-)

diff --git a/lib/modules/datasource/cdnjs/__snapshots__/index.spec.ts.snap b/lib/modules/datasource/cdnjs/__snapshots__/index.spec.ts.snap
index c779cc09bb..e1aead3be8 100644
--- a/lib/modules/datasource/cdnjs/__snapshots__/index.spec.ts.snap
+++ b/lib/modules/datasource/cdnjs/__snapshots__/index.spec.ts.snap
@@ -6,6 +6,7 @@ exports[`modules/datasource/cdnjs/index getReleases filters releases by asset pr
   "registryUrl": "https://api.cdnjs.com/",
   "releases": [
     {
+      "newDigest": undefined,
       "version": "0.7.5",
     },
   ],
diff --git a/lib/modules/datasource/galaxy-collection/__snapshots__/index.spec.ts.snap b/lib/modules/datasource/galaxy-collection/__snapshots__/index.spec.ts.snap
index 738884ca31..a02c616cd3 100644
--- a/lib/modules/datasource/galaxy-collection/__snapshots__/index.spec.ts.snap
+++ b/lib/modules/datasource/galaxy-collection/__snapshots__/index.spec.ts.snap
@@ -58,5 +58,6 @@ exports[`modules/datasource/galaxy-collection/index getReleases returns only val
       "version": "1.2.0",
     },
   ],
+  "tags": undefined,
 }
 `;
diff --git a/lib/modules/datasource/index.ts b/lib/modules/datasource/index.ts
index a9998eb83f..f175c8347b 100644
--- a/lib/modules/datasource/index.ts
+++ b/lib/modules/datasource/index.ts
@@ -353,7 +353,7 @@ export async function getPkgReleases(
     logger.error({ config }, 'Datasource getReleases without packageName');
     return null;
   }
-  let res: ReleaseResult;
+  let res: ReleaseResult | null = null;
   try {
     res = clone(
       await getRawReleases({
diff --git a/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap b/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap
index a174e16de4..02cf5b7c7e 100644
--- a/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap
+++ b/lib/modules/datasource/npm/__snapshots__/index.spec.ts.snap
@@ -6,10 +6,16 @@ exports[`modules/datasource/npm/index should fetch package info from custom regi
   "registryUrl": "https://npm.mycustomregistry.com",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -28,10 +34,16 @@ exports[`modules/datasource/npm/index should fetch package info from npm 1`] = `
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -50,10 +62,16 @@ exports[`modules/datasource/npm/index should handle foobar 1`] = `
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -72,10 +90,16 @@ exports[`modules/datasource/npm/index should handle no time 1`] = `
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "version": "0.0.2",
     },
   ],
@@ -93,10 +117,16 @@ exports[`modules/datasource/npm/index should not send an authorization header if
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -115,6 +145,9 @@ exports[`modules/datasource/npm/index should parse repo url (string) 1`] = `
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
@@ -132,6 +165,9 @@ exports[`modules/datasource/npm/index should parse repo url 1`] = `
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
@@ -149,10 +185,16 @@ exports[`modules/datasource/npm/index should replace any environment variable in
   "registryUrl": "https://registry.from-env.com",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -176,10 +218,16 @@ Marking the latest version of an npm package as deprecated results in the entire
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "isDeprecated": true,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
@@ -206,10 +254,16 @@ exports[`modules/datasource/npm/index should send an authorization header if pro
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -228,10 +282,16 @@ exports[`modules/datasource/npm/index should use default registry if missing fro
   "registryUrl": "https://registry.npmjs.org",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -250,10 +310,16 @@ exports[`modules/datasource/npm/index should use host rules by baseUrl if provid
   "registryUrl": "https://npm.mycustomregistry.com/_packaging/mycustomregistry/npm/registry",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
@@ -272,10 +338,16 @@ exports[`modules/datasource/npm/index should use host rules by hostName if provi
   "registryUrl": "https://npm.mycustomregistry.com",
   "releases": [
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-06T05:21:53.000Z",
       "version": "0.0.1",
     },
     {
+      "dependencies": undefined,
+      "devDependencies": undefined,
+      "gitRef": undefined,
       "releaseTimestamp": "2018-05-07T05:21:53.000Z",
       "version": "0.0.2",
     },
diff --git a/lib/util/clone.spec.ts b/lib/util/clone.spec.ts
index 6e7ade68f1..dc03323d66 100644
--- a/lib/util/clone.spec.ts
+++ b/lib/util/clone.spec.ts
@@ -1,19 +1,20 @@
 import { clone } from './clone';
 
 describe('util/clone', () => {
-  const obj: any = {
-    name: 'object',
-    type: 'object',
-    isObject: true,
-  };
-
   it('returns null', () => {
     const res = clone(null);
     expect(res).toBeNull();
   });
 
   it('maintains same order', () => {
+    const obj: any = {
+      name: 'object',
+      type: 'object',
+      isObject: true,
+    };
+
     const res = clone(obj);
+
     expect(res).toMatchSnapshot(`{
       name: 'object',
       type: 'object',
@@ -22,8 +23,18 @@ describe('util/clone', () => {
   });
 
   it('assigns "[Circular]" to circular references', () => {
+    const obj: any = {
+      name: 'object',
+      type: 'object',
+      isObject: true,
+    };
     obj.circular = obj;
-    const res = clone(obj);
-    expect(res.circular).toBe('[Circular]');
+
+    expect(clone(obj)).toMatchObject({
+      circular: '[Circular]',
+      isObject: true,
+      name: 'object',
+      type: 'object',
+    });
   });
 });
diff --git a/lib/util/clone.ts b/lib/util/clone.ts
index 422add0b37..93a81cdcb1 100644
--- a/lib/util/clone.ts
+++ b/lib/util/clone.ts
@@ -1,3 +1,5 @@
+import { klona } from 'klona/json';
+import { logger } from '../logger';
 import { quickStringify } from './stringify';
 
 /**
@@ -5,7 +7,17 @@ import { quickStringify } from './stringify';
  * @deprecated Use {@link structuredClone} instead.
  * @param input The object to clone.
  */
-export function clone<T>(input: T | null = null): T {
-  const stringifiedInput = quickStringify(input);
-  return stringifiedInput ? JSON.parse(stringifiedInput) : null;
+export function clone<T = unknown>(input: T): T {
+  try {
+    return klona(input);
+  } catch (err) {
+    logger.warn({ err }, 'error cloning object');
+    const str = quickStringify(input);
+    if (str) {
+      return JSON.parse(str);
+    }
+
+    // istanbul ignore next: not easily testable
+    throw err;
+  }
 }
diff --git a/package.json b/package.json
index e5f5a0348f..5b2d54eeb6 100644
--- a/package.json
+++ b/package.json
@@ -209,6 +209,7 @@
     "json-dup-key-validator": "1.0.3",
     "json-stringify-pretty-compact": "3.0.0",
     "json5": "2.2.3",
+    "klona": "2.0.6",
     "luxon": "3.3.0",
     "markdown-it": "13.0.1",
     "markdown-table": "2.0.0",
-- 
GitLab