Skip to content

Commit b9a944b

Browse files
chore(release): 1.0.7 [skip ci]
## [1.0.7](v1.0.6...v1.0.7) (2025-12-02) ### Bug Fixes * **logging:** add logging for file search patterns and replacement results in prepare function ([a6d9688](a6d9688))
1 parent a6d9688 commit b9a944b

File tree

12 files changed

+722
-18
lines changed

12 files changed

+722
-18
lines changed

HISTORY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## [1.0.7](https://github.com/centralnicgroup-opensource/rtldev-middleware-semantic-release-replace-plugin/compare/v1.0.6...v1.0.7) (2025-12-02)
2+
3+
4+
### Bug Fixes
5+
6+
* **logging:** add logging for file search patterns and replacement results in prepare function ([a6d9688](https://github.com/centralnicgroup-opensource/rtldev-middleware-semantic-release-replace-plugin/commit/a6d96886d7590a3f455f458aaf93dfbc75f80517))
7+
18
## [1.0.6](https://github.com/centralnicgroup-opensource/rtldev-middleware-semantic-release-replace-plugin/compare/v1.0.5...v1.0.6) (2025-10-30)
29

310

dist/index.d.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { VerifyReleaseContext as Context } from "semantic-release";
2+
export type From = FromCallback | RegExp | string;
3+
export type FromCallback = (filename: string, ...args: unknown[]) => RegExp | string;
4+
export type To = string | ToCallback;
5+
export type ToCallback = (match: string, ...args: unknown[]) => string;
6+
/**
7+
* Replacement is similar to the interface used by https://www.npmjs.com/package/replace-in-file
8+
* with the difference being the single string for `to` and `from`.
9+
*/
10+
export interface Replacement {
11+
/**
12+
* files to search for replacements
13+
*/
14+
files: string[];
15+
/**
16+
* The RegExp pattern to use to match.
17+
*
18+
* Uses `String.replace(new RegExp(s, 'gm'), to)` for implementation, if
19+
* `from` is a string.
20+
*
21+
* For advanced matching, i.e. when using a `release.config.js` file, consult
22+
* the documentation of the `replace-in-file` package
23+
* (https://github.com/adamreisnz/replace-in-file/blob/main/README.md) on its
24+
* `from` option. This allows explicit specification of `RegExp`s, callback
25+
* functions, etc.
26+
*
27+
* Multiple matchers may be provided as an array, following the same
28+
* conversion rules as mentioned above.
29+
*/
30+
from: From | From[];
31+
/**
32+
* The replacement value using a template of variables.
33+
*
34+
* `__VERSION__ = "${context.nextRelease.version}"`
35+
*
36+
* The context object is used to render the template. Additional values
37+
* can be found at: https://semantic-release.gitbook.io/semantic-release/developer-guide/js-api#result
38+
*
39+
* For advanced replacement (NOTE: only for use with `release.config.js` file version), pass in a function to replace non-standard variables
40+
* ```
41+
* {
42+
* from: `__VERSION__ = 11`, // eslint-disable-line
43+
* to: (matched) => `__VERSION: ${parseInt(matched.split('=')[1].trim()) + 1}`, // eslint-disable-line
44+
* },
45+
* ```
46+
*
47+
* The `args` for a callback function can take a variety of shapes. In its
48+
* simplest form, e.g. if `from` is a string, it's the filename in which the
49+
* replacement is done. If `from` is a regular expression the `args` of the
50+
* callback include captures, the offset of the matched string, the matched
51+
* string, etc. See the `String.replace` documentation for details
52+
*
53+
* Multiple replacements may be specified as an array. These can be either
54+
* strings or callback functions. Note that the amount of replacements needs
55+
* to match the amount of `from` matchers.
56+
*/
57+
to: To | To[];
58+
ignore?: string[];
59+
allowEmptyPaths?: boolean;
60+
countMatches?: boolean;
61+
disableGlobs?: boolean;
62+
encoding?: string;
63+
dry?: boolean;
64+
/**
65+
* The results array can be passed to ensure that the expected replacements
66+
* have been made, and if not, throw and exception with the diff.
67+
*/
68+
results?: {
69+
file: string;
70+
hasChanged: boolean;
71+
numMatches?: number;
72+
numReplacements?: number;
73+
}[];
74+
}
75+
/**
76+
* PluginConfig is used to provide multiple replacement.
77+
*
78+
* ```
79+
* [
80+
* "@google/semantic-release-replace-plugin",
81+
* {
82+
* "replacements": [
83+
* {
84+
* "files": ["foo/__init__.py"],
85+
* "from": "__VERSION__ = \".*\"",
86+
* "to": "__VERSION__ = \"${context.nextRelease.version}\"",
87+
* "results": [
88+
* {
89+
* "file": "foo/__init__.py",
90+
* "hasChanged": true,
91+
* "numMatches": 1,
92+
* "numReplacements": 1
93+
* }
94+
* ],
95+
* "countMatches": true
96+
* }
97+
* ]
98+
* }
99+
* ]
100+
* ```
101+
*/
102+
export interface PluginConfig {
103+
/** An array of replacements to be made. */
104+
replacements: Replacement[];
105+
}
106+
export declare function prepare(PluginConfig: PluginConfig, context: Context): Promise<void>;

dist/index.js

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { replaceInFile } from "replace-in-file";
2+
/**
3+
* Wraps the `callback` in a new function that passes the `context` as the
4+
* final argument to the `callback` when it gets called.
5+
*/
6+
function applyContextToCallback(callback, context) {
7+
return (...args) => callback.apply(null, args.concat(context));
8+
}
9+
/**
10+
* Applies the `context` to the replacement property `to` depending on whether
11+
* it is a string template or a callback function.
12+
*/
13+
function applyContextToReplacement(to, context) {
14+
return typeof to === "function"
15+
? applyContextToCallback(to, context)
16+
: new Function(...Object.keys(context), `return \`${to}\`;`)(...Object.values(context));
17+
}
18+
/**
19+
* Normalizes a `value` into an array, making it more straightforward to apply
20+
* logic to a single value of type `T` or an array of those values.
21+
*/
22+
function normalizeToArray(value) {
23+
return value instanceof Array ? value : [value];
24+
}
25+
/**
26+
* Compares two values for deep equality.
27+
*
28+
* This function handles complex data types such as `RegExp`, `Date`, `Map`, `Set`,
29+
* and performs deep comparison of nested objects and arrays.
30+
*
31+
* @param {any} a - The first value to compare.
32+
* @param {any} b - The second value to compare.
33+
* @returns {boolean} `true` if the values are deeply equal, `false` otherwise.
34+
*
35+
* @example
36+
* const obj1 = { regex: /abc/g, date: new Date(), set: new Set([1, 2, 3]) };
37+
* const obj2 = { regex: /abc/g, date: new Date(), set: new Set([1, 2, 3]) };
38+
*
39+
* console.log(deepEqual(obj1, obj2)); // true
40+
*
41+
* @example
42+
* const obj1 = { regex: /abc/g, date: new Date(2022, 0, 1) };
43+
* const obj2 = { regex: /abc/g, date: new Date(2021, 0, 1) };
44+
*
45+
* console.log(deepEqual(obj1, obj2)); // false
46+
*/
47+
function deepEqual(a, b) {
48+
if (a === b)
49+
return true; // Handle primitives
50+
// Check for null or undefined
51+
if (a == null || b == null)
52+
return false;
53+
// Handle RegExp
54+
if (a instanceof RegExp && b instanceof RegExp) {
55+
return a.source === b.source && a.flags === b.flags;
56+
}
57+
// Handle Date
58+
if (a instanceof Date && b instanceof Date) {
59+
return a.getTime() === b.getTime();
60+
}
61+
// Handle Map and Set
62+
if (a instanceof Map && b instanceof Map) {
63+
if (a.size !== b.size)
64+
return false;
65+
for (let [key, value] of a) {
66+
if (!b.has(key) || !deepEqual(value, b.get(key)))
67+
return false;
68+
}
69+
return true;
70+
}
71+
if (a instanceof Set && b instanceof Set) {
72+
if (a.size !== b.size)
73+
return false;
74+
for (let item of a) {
75+
if (!b.has(item))
76+
return false;
77+
}
78+
return true;
79+
}
80+
// Handle objects and arrays
81+
if (typeof a === "object" && typeof b === "object") {
82+
const keysA = Object.keys(a);
83+
const keysB = Object.keys(b);
84+
if (keysA.length !== keysB.length)
85+
return false;
86+
for (let key of keysA) {
87+
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
88+
return false;
89+
}
90+
}
91+
return true;
92+
}
93+
// If none of the checks match, return false
94+
return false;
95+
}
96+
/**
97+
* Recursively compares two objects and returns an array of differences.
98+
*
99+
* The function traverses the two objects (or arrays) and identifies differences
100+
* in their properties or elements. It supports complex types like `Date`, `RegExp`,
101+
* `Map`, `Set`, and checks nested objects and arrays.
102+
*
103+
* @param {any} obj1 - The first value to compare.
104+
* @param {any} obj2 - The second value to compare.
105+
* @param {string} [path=""] - The current path to the property or element being compared (used for recursion).
106+
* @returns {string[]} An array of strings representing the differences between the two values.
107+
*
108+
* @example
109+
* const obj1 = { a: 1, b: { c: 2 } };
110+
* const obj2 = { a: 1, b: { c: 3 } };
111+
*
112+
* const differences = deepDiff(obj1, obj2);
113+
* console.log(differences); // ['Difference at b.c: 2 !== 3']
114+
*
115+
* @example
116+
* const set1 = new Set([1, 2, 3]);
117+
* const set2 = new Set([1, 2, 4]);
118+
*
119+
* const differences = deepDiff(set1, set2);
120+
* console.log(differences); // ['Difference at : Set { 1, 2, 3 } !== Set { 1, 2, 4 }']
121+
*/
122+
function deepDiff(obj1, obj2, path = "") {
123+
let differences = [];
124+
if (typeof obj1 !== "object" ||
125+
typeof obj2 !== "object" ||
126+
obj1 === null ||
127+
obj2 === null) {
128+
if (obj1 !== obj2) {
129+
differences.push(`Difference at ${path}: ${obj1} !== ${obj2}`);
130+
}
131+
return differences;
132+
}
133+
// Check for Map or Set
134+
if (obj1 instanceof Map && obj2 instanceof Map) {
135+
if (obj1.size !== obj2.size) {
136+
differences.push(`Difference at ${path}: Map sizes do not match`);
137+
}
138+
for (let [key, value] of obj1) {
139+
if (!obj2.has(key) || !deepEqual(value, obj2.get(key))) {
140+
differences.push(`Difference at ${path}.${key}: ${value} !== ${obj2.get(key)}`);
141+
}
142+
}
143+
return differences;
144+
}
145+
if (obj1 instanceof Set && obj2 instanceof Set) {
146+
if (obj1.size !== obj2.size) {
147+
differences.push(`Difference at ${path}: Set sizes do not match`);
148+
}
149+
for (let item of obj1) {
150+
if (!obj2.has(item)) {
151+
differences.push(`Difference at ${path}: Set items do not match`);
152+
}
153+
}
154+
return differences;
155+
}
156+
// Handle RegExp
157+
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
158+
if (obj1.source !== obj2.source || obj1.flags !== obj2.flags) {
159+
differences.push(`Difference at ${path}: RegExp ${obj1} !== ${obj2}`);
160+
}
161+
return differences;
162+
}
163+
// Handle Date
164+
if (obj1 instanceof Date && obj2 instanceof Date) {
165+
if (obj1.getTime() !== obj2.getTime()) {
166+
differences.push(`Difference at ${path}: Date ${obj1} !== ${obj2}`);
167+
}
168+
return differences;
169+
}
170+
// Handle objects and arrays
171+
const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
172+
for (const key of keys) {
173+
const newPath = path ? `${path}.${key}` : key;
174+
differences = differences.concat(deepDiff(obj1[key], obj2[key], newPath));
175+
}
176+
return differences;
177+
}
178+
export async function prepare(PluginConfig, context) {
179+
for (const replacement of PluginConfig.replacements) {
180+
let { results } = replacement;
181+
delete replacement.results;
182+
// Log file patterns being searched
183+
context.logger?.log?.(`🔍 Searching for files matching: ${JSON.stringify(replacement.files)}`);
184+
const replaceInFileConfig = {
185+
...replacement,
186+
from: replacement.from ?? [],
187+
to: replacement.to ?? [],
188+
};
189+
replaceInFileConfig.from = normalizeToArray(replacement.from).map((from) => {
190+
switch (typeof from) {
191+
case "function":
192+
return applyContextToCallback(from, context);
193+
case "string":
194+
return new RegExp(from, "gm");
195+
default:
196+
return from;
197+
}
198+
});
199+
replaceInFileConfig.to =
200+
replacement.to instanceof Array
201+
? replacement.to.map((to) => applyContextToReplacement(to, context))
202+
: applyContextToReplacement(replacement.to, context);
203+
let actual = await replaceInFile(replaceInFileConfig);
204+
// Log results of replacement
205+
if (actual && actual.length > 0) {
206+
context.logger?.log?.(`✅ Files processed: ${actual.length} file(s) matched and updated`);
207+
actual.forEach((file) => {
208+
context.logger?.log?.(` 📄 ${file.file}: ${file.numReplacements ?? 0} replacement(s) made (${file.numMatches ?? 0} match(es))`);
209+
});
210+
}
211+
else {
212+
context.logger?.warn?.(`⚠️ No files found matching pattern: ${JSON.stringify(replacement.files)}`);
213+
}
214+
if (results) {
215+
results = results.sort();
216+
actual = actual.sort();
217+
if (!deepEqual([...actual].sort(), [...results].sort())) {
218+
const difference = deepDiff(actual, results);
219+
const errorMsg = [
220+
"❌ Replacement validation failed!",
221+
"",
222+
"Expected results did not match actual results.",
223+
"",
224+
"Possible causes:",
225+
" • File glob pattern didn't match expected files",
226+
" • Regex pattern didn't find expected matches",
227+
" • Check for proper escaping in JSON (use \\\\ for backslash)",
228+
" • Verify numMatches and numReplacements expectations",
229+
"",
230+
"Details:",
231+
...difference.map(d => ` ${d}`),
232+
].join("\n");
233+
throw new Error(errorMsg);
234+
}
235+
}
236+
}
237+
}

dist/index.test.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {};

0 commit comments

Comments
 (0)