Repository: JoshuaKGoldberg/bingo (ts)

Issue:
🐛 Bug: Template prepare()s objects should not override options provided by the CLI
### 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

Template `prepare()` functions should objects whose properties are supposed to fill in options that are not provided by the CLI. If something is provided by the CLI, it shouldn't take from the `prepare()` object.

### Actual

I'm seeing that passing `--owner` to `npx create-typescript-app` is being ignored, and CTA's default owner inference is being used instead.

### Additional Info

💝 

Pull request description:
fix(bingo): prefer existing options over prepared in prepareOptions
## PR Checklist

- [x] Addresses an existing open issue: fixes #262
- [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

I didn't love putting the `owner: z.string().optional()` directly in `createTemplate`. It's only relevant for setup mode. So now it lives alongside `directory` and `repository` in `runModeSetup`.

💝 

Patch:
diff --git a/packages/bingo/src/cli/setup/runModeSetup.ts b/packages/bingo/src/cli/setup/runModeSetup.ts
index 08fba7a..504c3c9 100644
--- a/packages/bingo/src/cli/setup/runModeSetup.ts
+++ b/packages/bingo/src/cli/setup/runModeSetup.ts
@@ -74,6 +74,7 @@ export async function runModeSetup<OptionsShape extends AnyShape, Refinements>({
 
 	const providedOptions = parseZodArgs(args, {
 		directory: z.string().optional(),
+		owner: z.string().optional(),
 		repository: z.string().optional(),
 		...template.options,
 	});
diff --git a/packages/bingo/src/creators/createTemplate.ts b/packages/bingo/src/creators/createTemplate.ts
index 4fcdac3..6066177 100644
--- a/packages/bingo/src/creators/createTemplate.ts
+++ b/packages/bingo/src/creators/createTemplate.ts
@@ -1,5 +1,3 @@
-import { z } from "zod";
-
 // eslint-disable-next-line @eslint-community/eslint-comments/disable-enable-pair
 /* eslint-disable @typescript-eslint/no-empty-object-type */
 import { AnyShape } from "../types/shapes.js";
@@ -22,12 +20,8 @@ export function createTemplate<OptionsShape extends AnyShape, Refinements>(
 ): Template<OptionsShape, Refinements> {
 	const template: Template<OptionsShape, Refinements> = {
 		createConfig: (config) => ({ ...config, template }),
-		options: {
-			owner: z.string().optional(),
-			// Fudging the types a bit: without options, the shape is {}.
-			// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-			...definition.options!,
-		},
+		// Fudging the types a bit: without options, the shape is {}.
+		options: (definition.options ?? {}) as OptionsShape,
 		...definition,
 	};
 
diff --git a/packages/bingo/src/preparation/prepareOptions.ts b/packages/bingo/src/preparation/prepareOptions.ts
index b7ebf8b..ddd9376 100644
--- a/packages/bingo/src/preparation/prepareOptions.ts
+++ b/packages/bingo/src/preparation/prepareOptions.ts
@@ -55,12 +55,13 @@ export async function prepareOptions<OptionsShape extends AnyShape>(
 			exclude: /node_modules|^\.git$/,
 		})) as CreatedDirectory);
 
-	return await awaitLazyProperties(
-		base.prepare({
+	return await awaitLazyProperties({
+		...base.prepare({
 			files,
 			log: system.display.log,
 			options: existing,
 			...system,
 		}),
-	);
+		...existing,
+	});
 }


Test patch:
diff --git a/packages/bingo/src/creators/createTemplate.test.ts b/packages/bingo/src/creators/createTemplate.test.ts
deleted file mode 100644
index e878622..0000000
--- a/packages/bingo/src/creators/createTemplate.test.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { describe, expect, it, vi } from "vitest";
-import { z } from "zod";
-
-import { createTemplate } from "./createTemplate.js";
-
-describe("createTemplate", () => {
-	it("adds options.owner when the definition options don't include owner", () => {
-		const template = createTemplate({
-			produce: vi.fn(),
-		});
-
-		expect(template.options.owner).toBeInstanceOf(z.ZodOptional);
-	});
-
-	it("uses definition.options.owner when it exists", () => {
-		const owner = z.string();
-		const template = createTemplate({
-			options: { owner },
-			produce: vi.fn(),
-		});
-
-		expect(template.options.owner).toBe(owner);
-	});
-});
diff --git a/packages/bingo/src/preparation/prepareOptions.test.ts b/packages/bingo/src/preparation/prepareOptions.test.ts
index 96f0376..f13c7ed 100644
--- a/packages/bingo/src/preparation/prepareOptions.test.ts
+++ b/packages/bingo/src/preparation/prepareOptions.test.ts
@@ -50,14 +50,16 @@ describe("prepareOptions", () => {
 
 	describe("prepare", () => {
 		const baseWithOptionalOption: HasOptionsAndMaybePrepare<{
-			value: z.ZodOptional<z.ZodString>;
+			explicit: z.ZodOptional<z.ZodString>;
+			inferred: z.ZodOptional<z.ZodString>;
 		}> = {
 			options: {
-				value: z.string().optional(),
+				explicit: z.string().optional(),
+				inferred: z.string().optional(),
 			},
 			prepare({ options }) {
 				return {
-					value: options.value ?? "default",
+					explicit: options.explicit ?? "explicit-default",
 				};
 			},
 		};
@@ -65,7 +67,7 @@ describe("prepareOptions", () => {
 		it("uses an option value from produce when settings do not have options", async () => {
 			const actual = await prepareOptions(baseWithOptionalOption, system);
 
-			expect(actual).toEqual({ value: "default" });
+			expect(actual).toEqual({ explicit: "explicit-default" });
 		});
 
 		it("uses an option value from produce when settings do not have the options value", async () => {
@@ -74,28 +76,32 @@ describe("prepareOptions", () => {
 				existing: {},
 			});
 
-			expect(actual).toEqual({ value: "default" });
+			expect(actual).toEqual({ explicit: "explicit-default" });
 		});
 
 		it("uses an option value from settings when settings have the options value", async () => {
 			const actual = await prepareOptions(baseWithOptionalOption, {
 				...system,
 				existing: {
-					value: "override",
+					explicit: "explicit-override",
+					inferred: "inferred-override",
 				},
 			});
 
-			expect(actual).toEqual({ value: "override" });
+			expect(actual).toEqual({
+				explicit: "explicit-override",
+				inferred: "inferred-override",
+			});
 		});
 
 		it("prepares options when given a template", async () => {
 			const template = createTemplate({
 				options: {
-					value: z.string(),
+					explicit: z.string(),
 				},
 				prepare() {
 					return {
-						value: () => Promise.resolve("default"),
+						explicit: () => Promise.resolve("default"),
 					};
 				},
 				produce() {
@@ -105,7 +111,7 @@ describe("prepareOptions", () => {
 
 			const actual = await prepareOptions(template);
 
-			expect(actual).toEqual({ value: "default" });
+			expect(actual).toEqual({ explicit: "default" });
 		});
 	});
 });