Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

18 changes: 0 additions & 18 deletions .eslintrc.json

This file was deleted.

3 changes: 1 addition & 2 deletions .github/linters/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@
"skipLibCheck": true,
"moduleResolution": "NodeNext"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
"include": ["src/**/*.ts"]
}
19 changes: 2 additions & 17 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { VerifyReleaseContext as Context } from "semantic-release";
export type From = FromCallback | RegExp | string;
export type FromCallback = (filename: string, ...args: unknown[]) => RegExp | string;
Expand Down Expand Up @@ -46,7 +31,7 @@ export interface Replacement {
/**
* The replacement value using a template of variables.
*
* `__VERSION__ = "${nextRelease.version}"`
* `__VERSION__ = "${context.nextRelease.version}"`
*
* The context object is used to render the template. Additional values
* can be found at: https://semantic-release.gitbook.io/semantic-release/developer-guide/js-api#result
Expand Down Expand Up @@ -98,7 +83,7 @@ export interface Replacement {
* {
* "files": ["foo/__init__.py"],
* "from": "__VERSION__ = \".*\"",
* "to": "__VERSION__ = \"${nextRelease.version}\"",
* "to": "__VERSION__ = \"${context.nextRelease.version}\"",
* "results": [
* {
* "file": "foo/__init__.py",
Expand Down
195 changes: 159 additions & 36 deletions dist/index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,9 @@
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import replace from "replace-in-file";
import { isEqual, template } from "lodash-es";
import { diff } from "jest-diff";
import { replaceInFile } from "replace-in-file";
/**
* Wraps the `callback` in a new function that passes the `context` as the
* final argument to the `callback` when it gets called.
*/
// eslint-disable-next-line @typescript-eslint/ban-types
function applyContextToCallback(callback, context) {
// eslint-disable-next-line prefer-spread
return (...args) => callback.apply(null, args.concat(context));
}
/**
Expand All @@ -32,7 +13,7 @@ function applyContextToCallback(callback, context) {
function applyContextToReplacement(to, context) {
return typeof to === "function"
? applyContextToCallback(to, context)
: template(to)({ ...context });
: new Function(...Object.keys(context), `return \`${to}\`;`)(...Object.values(context));
}
/**
* Normalizes a `value` into an array, making it more straightforward to apply
Expand All @@ -41,6 +22,159 @@ function applyContextToReplacement(to, context) {
function normalizeToArray(value) {
return value instanceof Array ? value : [value];
}
/**
* Compares two values for deep equality.
*
* This function handles complex data types such as `RegExp`, `Date`, `Map`, `Set`,
* and performs deep comparison of nested objects and arrays.
*
* @param {any} a - The first value to compare.
* @param {any} b - The second value to compare.
* @returns {boolean} `true` if the values are deeply equal, `false` otherwise.
*
* @example
* const obj1 = { regex: /abc/g, date: new Date(), set: new Set([1, 2, 3]) };
* const obj2 = { regex: /abc/g, date: new Date(), set: new Set([1, 2, 3]) };
*
* console.log(deepEqual(obj1, obj2)); // true
*
* @example
* const obj1 = { regex: /abc/g, date: new Date(2022, 0, 1) };
* const obj2 = { regex: /abc/g, date: new Date(2021, 0, 1) };
*
* console.log(deepEqual(obj1, obj2)); // false
*/
function deepEqual(a, b) {
if (a === b)
return true; // Handle primitives
// Check for null or undefined
if (a == null || b == null)
return false;
// Handle RegExp
if (a instanceof RegExp && b instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
// Handle Date
if (a instanceof Date && b instanceof Date) {
return a.getTime() === b.getTime();
}
// Handle Map and Set
if (a instanceof Map && b instanceof Map) {
if (a.size !== b.size)
return false;
for (let [key, value] of a) {
if (!b.has(key) || !deepEqual(value, b.get(key)))
return false;
}
return true;
}
if (a instanceof Set && b instanceof Set) {
if (a.size !== b.size)
return false;
for (let item of a) {
if (!b.has(item))
return false;
}
return true;
}
// Handle objects and arrays
if (typeof a === "object" && typeof b === "object") {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length)
return false;
for (let key of keysA) {
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
// If none of the checks match, return false
return false;
}
/**
* Recursively compares two objects and returns an array of differences.
*
* The function traverses the two objects (or arrays) and identifies differences
* in their properties or elements. It supports complex types like `Date`, `RegExp`,
* `Map`, `Set`, and checks nested objects and arrays.
*
* @param {any} obj1 - The first value to compare.
* @param {any} obj2 - The second value to compare.
* @param {string} [path=""] - The current path to the property or element being compared (used for recursion).
* @returns {string[]} An array of strings representing the differences between the two values.
*
* @example
* const obj1 = { a: 1, b: { c: 2 } };
* const obj2 = { a: 1, b: { c: 3 } };
*
* const differences = deepDiff(obj1, obj2);
* console.log(differences); // ['Difference at b.c: 2 !== 3']
*
* @example
* const set1 = new Set([1, 2, 3]);
* const set2 = new Set([1, 2, 4]);
*
* const differences = deepDiff(set1, set2);
* console.log(differences); // ['Difference at : Set { 1, 2, 3 } !== Set { 1, 2, 4 }']
*/
function deepDiff(obj1, obj2, path = "") {
let differences = [];
if (typeof obj1 !== "object" ||
typeof obj2 !== "object" ||
obj1 === null ||
obj2 === null) {
if (obj1 !== obj2) {
differences.push(`Difference at ${path}: ${obj1} !== ${obj2}`);
}
return differences;
}
// Check for Map or Set
if (obj1 instanceof Map && obj2 instanceof Map) {
if (obj1.size !== obj2.size) {
differences.push(`Difference at ${path}: Map sizes do not match`);
}
for (let [key, value] of obj1) {
if (!obj2.has(key) || !deepEqual(value, obj2.get(key))) {
differences.push(`Difference at ${path}.${key}: ${value} !== ${obj2.get(key)}`);
}
}
return differences;
}
if (obj1 instanceof Set && obj2 instanceof Set) {
if (obj1.size !== obj2.size) {
differences.push(`Difference at ${path}: Set sizes do not match`);
}
for (let item of obj1) {
if (!obj2.has(item)) {
differences.push(`Difference at ${path}: Set items do not match`);
}
}
return differences;
}
// Handle RegExp
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
if (obj1.source !== obj2.source || obj1.flags !== obj2.flags) {
differences.push(`Difference at ${path}: RegExp ${obj1} !== ${obj2}`);
}
return differences;
}
// Handle Date
if (obj1 instanceof Date && obj2 instanceof Date) {
if (obj1.getTime() !== obj2.getTime()) {
differences.push(`Difference at ${path}: Date ${obj1} !== ${obj2}`);
}
return differences;
}
// Handle objects and arrays
const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
for (const key of keys) {
const newPath = path ? `${path}.${key}` : key;
differences = differences.concat(deepDiff(obj1[key], obj2[key], newPath));
}
return differences;
}
export async function prepare(PluginConfig, context) {
for (const replacement of PluginConfig.replacements) {
let { results } = replacement;
Expand All @@ -50,18 +184,6 @@ export async function prepare(PluginConfig, context) {
from: replacement.from ?? [],
to: replacement.to ?? [],
};
// The `replace-in-file` package uses `String.replace` under the hood for
// the actual replacement. If `from` is a string, this means only a
// single occurrence will be replaced. This plugin intents to replace
// _all_ occurrences when given a string to better support
// configuration through JSON, this requires conversion into a `RegExp`.
//
// If `from` is a callback function, the `context` is passed as the final
// parameter to the function. In all other cases, e.g. being a
// `RegExp`, the `from` property does not require any modifications.
//
// The `from` property may either be a single value to match or an array of
// values (in any of the previously described forms)
replaceInFileConfig.from = normalizeToArray(replacement.from).map((from) => {
switch (typeof from) {
case "function":
Expand All @@ -76,12 +198,13 @@ export async function prepare(PluginConfig, context) {
replacement.to instanceof Array
? replacement.to.map((to) => applyContextToReplacement(to, context))
: applyContextToReplacement(replacement.to, context);
let actual = await replace(replaceInFileConfig);
let actual = await replaceInFile(replaceInFileConfig);
if (results) {
results = results.sort();
actual = actual.sort();
if (!isEqual(actual.sort(), results.sort())) {
throw new Error(`Expected match not found!\n${diff(actual, results)}`);
if (!deepEqual([...actual].sort(), [...results].sort())) {
const difference = deepDiff(actual, results);
throw new Error(`Expected match not found!\n${difference.join("\n")}`);
}
}
}
Expand Down
15 changes: 0 additions & 15 deletions dist/index.test.d.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1 @@
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {};
22 changes: 5 additions & 17 deletions dist/index.test.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,3 @@
/**
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as m from "./index.js";
import fs from "fs-extra";
import path from "path";
Expand All @@ -22,7 +7,7 @@ import { prepare } from "./index.js";
const context = {
stdout: process.stdout,
stderr: process.stderr,
logger: {},
logger: {}, // You might need to provide appropriate options to Signale constructor
cwd: "/path/to/your/repository",
env: process.env,
envCi: {
Expand Down Expand Up @@ -97,6 +82,9 @@ const context = {
channel: "main",
notes: "These are the release notes for the next release.",
},
options: {
ci: false,
},
};
let d;
beforeEach(() => {
Expand Down Expand Up @@ -242,7 +230,7 @@ test("prepare accepts callback functions for `from`", async () => {
const replacements = [
{
files: [path.join(d.name, "/foo.md")],
from: (filename) => `${path.basename(filename, ".md")}@1.0.0`,
from: (filename) => `${path.basename(filename, ".md")}@1.0.0`, // Equivalent to "foo@1.0.0"
to: `foo@${context.nextRelease?.version}`,
},
];
Expand Down
1 change: 0 additions & 1 deletion docs/.nojekyll

This file was deleted.

Loading