1- /**
2- * Copyright 2020 Google LLC
3- *
4- * Licensed under the Apache License, Version 2.0 (the "License");
5- * you may not use this file except in compliance with the License.
6- * You may obtain a copy of the License at
7- *
8- * http://www.apache.org/licenses/LICENSE-2.0
9- *
10- * Unless required by applicable law or agreed to in writing, software
11- * distributed under the License is distributed on an "AS IS" BASIS,
12- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13- * See the License for the specific language governing permissions and
14- * limitations under the License.
15- */
16- import replace from "replace-in-file" ;
17- import { isEqual , template } from "lodash-es" ;
18- import { diff } from "jest-diff" ;
1+ import { replaceInFile } from "replace-in-file" ;
192/**
203 * Wraps the `callback` in a new function that passes the `context` as the
214 * final argument to the `callback` when it gets called.
@@ -32,7 +15,7 @@ function applyContextToCallback(callback, context) {
3215function applyContextToReplacement ( to , context ) {
3316 return typeof to === "function"
3417 ? applyContextToCallback ( to , context )
35- : template ( to ) ( { ...context } ) ;
18+ : new Function ( ... Object . keys ( context ) , `return \` ${ to } \`;` ) ( ...Object . values ( context ) ) ;
3619}
3720/**
3821 * Normalizes a `value` into an array, making it more straightforward to apply
@@ -41,6 +24,156 @@ function applyContextToReplacement(to, context) {
4124function normalizeToArray ( value ) {
4225 return value instanceof Array ? value : [ value ] ;
4326}
27+ /**
28+ * Compares two values for deep equality.
29+ *
30+ * This function handles complex data types such as `RegExp`, `Date`, `Map`, `Set`,
31+ * and performs deep comparison of nested objects and arrays.
32+ *
33+ * @param {any } a - The first value to compare.
34+ * @param {any } b - The second value to compare.
35+ * @returns {boolean } `true` if the values are deeply equal, `false` otherwise.
36+ *
37+ * @example
38+ * const obj1 = { regex: /abc/g, date: new Date(), set: new Set([1, 2, 3]) };
39+ * const obj2 = { regex: /abc/g, date: new Date(), set: new Set([1, 2, 3]) };
40+ *
41+ * console.log(deepEqual(obj1, obj2)); // true
42+ *
43+ * @example
44+ * const obj1 = { regex: /abc/g, date: new Date(2022, 0, 1) };
45+ * const obj2 = { regex: /abc/g, date: new Date(2021, 0, 1) };
46+ *
47+ * console.log(deepEqual(obj1, obj2)); // false
48+ */
49+ function deepEqual ( a , b ) {
50+ if ( a === b )
51+ return true ; // Handle primitives
52+ // Check for null or undefined
53+ if ( a == null || b == null )
54+ return false ;
55+ // Handle RegExp
56+ if ( a instanceof RegExp && b instanceof RegExp ) {
57+ return a . source === b . source && a . flags === b . flags ;
58+ }
59+ // Handle Date
60+ if ( a instanceof Date && b instanceof Date ) {
61+ return a . getTime ( ) === b . getTime ( ) ;
62+ }
63+ // Handle Map and Set
64+ if ( a instanceof Map && b instanceof Map ) {
65+ if ( a . size !== b . size )
66+ return false ;
67+ for ( let [ key , value ] of a ) {
68+ if ( ! b . has ( key ) || ! deepEqual ( value , b . get ( key ) ) )
69+ return false ;
70+ }
71+ return true ;
72+ }
73+ if ( a instanceof Set && b instanceof Set ) {
74+ if ( a . size !== b . size )
75+ return false ;
76+ for ( let item of a ) {
77+ if ( ! b . has ( item ) )
78+ return false ;
79+ }
80+ return true ;
81+ }
82+ // Handle objects and arrays
83+ if ( typeof a === "object" && typeof b === "object" ) {
84+ const keysA = Object . keys ( a ) ;
85+ const keysB = Object . keys ( b ) ;
86+ if ( keysA . length !== keysB . length )
87+ return false ;
88+ for ( let key of keysA ) {
89+ if ( ! keysB . includes ( key ) || ! deepEqual ( a [ key ] , b [ key ] ) ) {
90+ return false ;
91+ }
92+ }
93+ return true ;
94+ }
95+ // If none of the checks match, return false
96+ return false ;
97+ }
98+ /**
99+ * Recursively compares two objects and returns an array of differences.
100+ *
101+ * The function traverses the two objects (or arrays) and identifies differences
102+ * in their properties or elements. It supports complex types like `Date`, `RegExp`,
103+ * `Map`, `Set`, and checks nested objects and arrays.
104+ *
105+ * @param {any } obj1 - The first value to compare.
106+ * @param {any } obj2 - The second value to compare.
107+ * @param {string } [path=""] - The current path to the property or element being compared (used for recursion).
108+ * @returns {string[] } An array of strings representing the differences between the two values.
109+ *
110+ * @example
111+ * const obj1 = { a: 1, b: { c: 2 } };
112+ * const obj2 = { a: 1, b: { c: 3 } };
113+ *
114+ * const differences = deepDiff(obj1, obj2);
115+ * console.log(differences); // ['Difference at b.c: 2 !== 3']
116+ *
117+ * @example
118+ * const set1 = new Set([1, 2, 3]);
119+ * const set2 = new Set([1, 2, 4]);
120+ *
121+ * const differences = deepDiff(set1, set2);
122+ * console.log(differences); // ['Difference at : Set { 1, 2, 3 } !== Set { 1, 2, 4 }']
123+ */
124+ function deepDiff ( obj1 , obj2 , path = "" ) {
125+ let differences = [ ] ;
126+ if ( typeof obj1 !== "object" || typeof obj2 !== "object" || obj1 === null || obj2 === null ) {
127+ if ( obj1 !== obj2 ) {
128+ differences . push ( `Difference at ${ path } : ${ obj1 } !== ${ obj2 } ` ) ;
129+ }
130+ return differences ;
131+ }
132+ // Check for Map or Set
133+ if ( obj1 instanceof Map && obj2 instanceof Map ) {
134+ if ( obj1 . size !== obj2 . size ) {
135+ differences . push ( `Difference at ${ path } : Map sizes do not match` ) ;
136+ }
137+ for ( let [ key , value ] of obj1 ) {
138+ if ( ! obj2 . has ( key ) || ! deepEqual ( value , obj2 . get ( key ) ) ) {
139+ differences . push ( `Difference at ${ path } .${ key } : ${ value } !== ${ obj2 . get ( key ) } ` ) ;
140+ }
141+ }
142+ return differences ;
143+ }
144+ if ( obj1 instanceof Set && obj2 instanceof Set ) {
145+ if ( obj1 . size !== obj2 . size ) {
146+ differences . push ( `Difference at ${ path } : Set sizes do not match` ) ;
147+ }
148+ for ( let item of obj1 ) {
149+ if ( ! obj2 . has ( item ) ) {
150+ differences . push ( `Difference at ${ path } : Set items do not match` ) ;
151+ }
152+ }
153+ return differences ;
154+ }
155+ // Handle RegExp
156+ if ( obj1 instanceof RegExp && obj2 instanceof RegExp ) {
157+ if ( obj1 . source !== obj2 . source || obj1 . flags !== obj2 . flags ) {
158+ differences . push ( `Difference at ${ path } : RegExp ${ obj1 } !== ${ obj2 } ` ) ;
159+ }
160+ return differences ;
161+ }
162+ // Handle Date
163+ if ( obj1 instanceof Date && obj2 instanceof Date ) {
164+ if ( obj1 . getTime ( ) !== obj2 . getTime ( ) ) {
165+ differences . push ( `Difference at ${ path } : Date ${ obj1 } !== ${ obj2 } ` ) ;
166+ }
167+ return differences ;
168+ }
169+ // Handle objects and arrays
170+ const keys = new Set ( [ ...Object . keys ( obj1 ) , ...Object . keys ( obj2 ) ] ) ;
171+ for ( const key of keys ) {
172+ const newPath = path ? `${ path } .${ key } ` : key ;
173+ differences = differences . concat ( deepDiff ( obj1 [ key ] , obj2 [ key ] , newPath ) ) ;
174+ }
175+ return differences ;
176+ }
44177export async function prepare ( PluginConfig , context ) {
45178 for ( const replacement of PluginConfig . replacements ) {
46179 let { results } = replacement ;
@@ -50,18 +183,6 @@ export async function prepare(PluginConfig, context) {
50183 from : replacement . from ?? [ ] ,
51184 to : replacement . to ?? [ ] ,
52185 } ;
53- // The `replace-in-file` package uses `String.replace` under the hood for
54- // the actual replacement. If `from` is a string, this means only a
55- // single occurrence will be replaced. This plugin intents to replace
56- // _all_ occurrences when given a string to better support
57- // configuration through JSON, this requires conversion into a `RegExp`.
58- //
59- // If `from` is a callback function, the `context` is passed as the final
60- // parameter to the function. In all other cases, e.g. being a
61- // `RegExp`, the `from` property does not require any modifications.
62- //
63- // The `from` property may either be a single value to match or an array of
64- // values (in any of the previously described forms)
65186 replaceInFileConfig . from = normalizeToArray ( replacement . from ) . map ( ( from ) => {
66187 switch ( typeof from ) {
67188 case "function" :
@@ -76,12 +197,13 @@ export async function prepare(PluginConfig, context) {
76197 replacement . to instanceof Array
77198 ? replacement . to . map ( ( to ) => applyContextToReplacement ( to , context ) )
78199 : applyContextToReplacement ( replacement . to , context ) ;
79- let actual = await replace ( replaceInFileConfig ) ;
200+ let actual = await replaceInFile ( replaceInFileConfig ) ;
80201 if ( results ) {
81202 results = results . sort ( ) ;
82203 actual = actual . sort ( ) ;
83- if ( ! isEqual ( actual . sort ( ) , results . sort ( ) ) ) {
84- throw new Error ( `Expected match not found!\n${ diff ( actual , results ) } ` ) ;
204+ if ( ! deepEqual ( [ ...actual ] . sort ( ) , [ ...results ] . sort ( ) ) ) {
205+ const difference = deepDiff ( actual , results ) ;
206+ throw new Error ( `Expected match not found!\n${ difference . join ( "\n" ) } ` ) ;
85207 }
86208 }
87209 }
0 commit comments