Repository: JoshuaKGoldberg/bingo (ts)

Issue:
🐛 Bug: Stratum Addon merger error detection misses root-level arrays
### Bug Report Checklist

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have pulled the latest `main` branch of the repository.
- [x] I have [searched for related issues](https://github.com/JoshuaKGoldberg/bingo/issues?q=is%3Aissue) and found none that matched my issue.

### Expected

`mergeAddonsIfUpdate` is meant to return an error if an array is requested to be merged with a non-array

### Actual

In trying to get this `create-typescript-app.config.js` with `create-typescript-app@2.14.0`:

```js
import {
  blockCTATransitions,
  blockESLint,
  createConfig,
} from "../create-typescript-app/lib/index.js";

export default createConfig({
  refinements: {
    addons: [
      blockESLint({
        rules: { "n/no-unpublished-import": "off" },
      }),
    ],
    blocks: {
      add: [blockCTATransitions],
    },
  },
});
```

I see that `rules` is being merged as:

```js
{
  "0": ...,
  "n/no-unpublished-import": "off",
}
```

### Additional Info

💝 

Pull request:
fix(bingo-stratum): return Addon merging errors for root-level array mismatches
## PR Checklist

- [x] Addresses an existing open issue: fixes #315
- [x] That issue was marked as [`status: accepting prs`](https://github.com/JoshuaKGoldberg/bingo/issues?q=is%3Aopen+is%3Aissue+label%3A%22status%3A+accepting+prs%22)
- [x] Steps in [CONTRIBUTING.md](https://github.com/JoshuaKGoldberg/bingo/blob/main/.github/CONTRIBUTING.md) were taken

## Overview

This still doesn't _throw_ the error, so they're not yet appropriately handled outside of mergers. But it's progress.

💝 

Patch:
diff --git a/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.ts b/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.ts
index 5b6a08f..3ae3a0b 100644
--- a/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.ts
+++ b/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.ts
@@ -4,6 +4,15 @@ export function mergeAddonsIfUpdated<T extends object>(
 	existingAddons: T,
 	newAddons: T,
 ): Error | T | undefined {
+	if (Array.isArray(existingAddons)) {
+		if (!Array.isArray(newAddons)) {
+			return new Error("Mismatched merging addons (Array.isArray).");
+		}
+		return mergeAddonsArraysIfUpdated(existingAddons, newAddons) as T;
+	} else if (Array.isArray(newAddons)) {
+		return new Error("Mismatched merging addons (Array.isArray).");
+	}
+
 	const newEntries = Object.entries(newAddons) as [keyof T, unknown][];
 	const result = { ...existingAddons };
 	let updated = newEntries.length !== Object.keys(existingAddons).length;
@@ -73,3 +82,25 @@ function createHash(value: unknown) {
 		? hashObject(value as Record<string, unknown>)
 		: String(value as boolean | null | number | string | undefined);
 }
+
+function mergeAddonsArraysIfUpdated<T extends object>(
+	existingAddons: T[],
+	newAddons: T[],
+) {
+	const result = [...existingAddons];
+	const hashes = new Set(existingAddons.map(createHash));
+	let updated = false;
+
+	for (const newAddon of newAddons) {
+		const newHash = createHash(newAddon);
+		if (hashes.has(newHash)) {
+			continue;
+		}
+
+		hashes.add(newHash);
+		result.push(newAddon);
+		updated = true;
+	}
+
+	return updated ? result : undefined;
+}


Test patch:
diff --git a/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.test.ts b/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.test.ts
index 93b695c..7af6a39 100644
--- a/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.test.ts
+++ b/packages/bingo-stratum/src/mergers/mergeAddonsIfUpdated.test.ts
@@ -5,6 +5,11 @@ import { mergeAddonsIfUpdated } from "./mergeAddonsIfUpdated.js";
 describe("mergeArgsIfUpdated", () => {
 	it.each([
 		[{}, {}, undefined],
+		[[], [], undefined],
+		[[], {}, new Error("Mismatched merging addons (Array.isArray).")],
+		[{}, [], new Error("Mismatched merging addons (Array.isArray).")],
+		[["a"], ["a"], undefined],
+		[["a"], ["b"], ["a", "b"]],
 		[{ a: true }, {}, { a: true }],
 		[{ a: true }, { a: null }, undefined],
 		[{ a: true }, { a: true }, undefined],