diff --git a/.changeset/tidy-kv-placeholder.md b/.changeset/tidy-kv-placeholder.md new file mode 100644 index 0000000000..488cbcba32 --- /dev/null +++ b/.changeset/tidy-kv-placeholder.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Replace existing bindings when adding newly created resources to Wrangler configuration + +When config updates are authorized interactively or through `--update-config` or `--binding`, Wrangler now replaces an existing resource binding with the selected name instead of adding a duplicate entry. This allows template bindings with placeholder resource IDs to be updated in both interactive and non-interactive workflows. diff --git a/packages/wrangler/src/__tests__/update-config-file.test.ts b/packages/wrangler/src/__tests__/update-config-file.test.ts index 4c84f7e8f4..6cabc16d17 100644 --- a/packages/wrangler/src/__tests__/update-config-file.test.ts +++ b/packages/wrangler/src/__tests__/update-config-file.test.ts @@ -142,6 +142,96 @@ describe("createdResourceConfig()", () => { ); }); + it("interactive: replaces an existing binding with the same name", async ({ + expect, + }) => { + writeWranglerConfig( + { + name: "worker", + kv_namespaces: [ + { binding: "OTHER_KV", id: "other-id" }, + { + binding: "KV", + id: "", + preview_id: "preview-id", + remote: true, + }, + ], + }, + "wrangler.json" + ); + + setIsTTY(true); + mockConfirm({ + text: "Would you like Wrangler to add it on your behalf?", + result: true, + }); + mockPrompt({ + text: "What binding name would you like to use?", + result: "KV", + }); + mockConfirm({ + text: "For local dev, do you want to connect to the remote resource instead of a local resource?", + result: false, + }); + + await createdResourceConfig( + "kv_namespaces", + (name) => ({ binding: name ?? "KV", id: "random-id" }), + "wrangler.json", + undefined + ); + + expect(await readFile("wrangler.json", "utf8")).toMatchInlineSnapshot(` + "{ + "compatibility_date": "2022-01-12", + "name": "worker", + "kv_namespaces": [ + { + "binding": "OTHER_KV", + "id": "other-id" + }, + { + "binding": "KV", + "id": "random-id", + "preview_id": "preview-id" + } + ] + }" + `); + }); + + it("non interactive: replaces an existing binding with the same name", async ({ + expect, + }) => { + writeWranglerConfig( + { + name: "worker", + kv_namespaces: [ + { + binding: "KV", + id: "", + remote: true, + }, + ], + }, + "wrangler.json" + ); + + await createdResourceConfig( + "kv_namespaces", + (name) => ({ binding: name ?? "KV", id: "random-id" }), + "wrangler.json", + undefined, + { binding: "KV", updateConfig: true } + ); + + expect(await readFile("wrangler.json", "utf8")).toContain( + '"id": "random-id"' + ); + expect(await readFile("wrangler.json", "utf8")).not.toContain('"remote"'); + }); + it("interactive: file update in env after answering yes", async ({ expect, }) => { diff --git a/packages/wrangler/src/utils/add-created-resource-config.ts b/packages/wrangler/src/utils/add-created-resource-config.ts index 6bc2e83730..eb3f11af12 100644 --- a/packages/wrangler/src/utils/add-created-resource-config.ts +++ b/packages/wrangler/src/utils/add-created-resource-config.ts @@ -1,5 +1,6 @@ import { configFormat, + experimental_readRawConfig, experimental_patchConfig, formatConfigSnippet, friendlyBindingNames, @@ -129,16 +130,39 @@ export async function createdResourceConfig( { defaultValue: false } )); - const configFilePatch = { - [resource]: [ - { ...snippet(bindingName), ...(useRemote ? { remote: true } : {}) }, - ], + const { rawConfig } = experimental_readRawConfig({ config: configPath }); + const environment = env ? rawConfig.env?.[env] : rawConfig; + + const binding = { + ...snippet(bindingName), + remote: useRemote ? true : undefined, }; + const existingBindings = environment?.[resource]; + const matchingBindingIndex = existingBindings?.findIndex( + (existingBinding) => existingBinding.binding === binding.binding + ); + + const shouldReplaceExistingBinding = + existingBindings !== undefined && + matchingBindingIndex !== undefined && + matchingBindingIndex !== -1; + + let bindings: Partial[number]>[] = [binding]; + if (shouldReplaceExistingBinding) { + bindings = existingBindings.map((existingBinding, index) => { + if (index !== matchingBindingIndex) { + return existingBinding; + } + return { ...existingBinding, ...binding }; + }); + } + + const configFilePatch = { [resource]: bindings }; experimental_patchConfig( configPath, env ? { env: { [env]: configFilePatch } } : configFilePatch, - true + !shouldReplaceExistingBinding ); } }