Skip to content

Commit 86b763f

Browse files
committed
thomasgauvin: made config-binding-name a string arg instead of boolean, added interactive prompting for binding name
1 parent 9d4a32f commit 86b763f

File tree

9 files changed

+453
-561
lines changed

9 files changed

+453
-561
lines changed

packages/wrangler/src/__tests__/config/auto-update.test.ts

Lines changed: 94 additions & 242 deletions
Large diffs are not rendered by default.

packages/wrangler/src/__tests__/d1/create.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ describe("create", () => {
6464
"✅ Successfully created DB 'test' in region OC
6565
Created your new D1 database.
6666
67+
Add the following to your configuration file:
68+
6769
{
6870
\\"d1_databases\\": [
6971
{
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { confirm, prompt } from "../dialogs";
2+
import { logger } from "../logger";
3+
import { isValidIdentifier } from "../type-generation";
4+
import { formatConfigSnippet } from "./index";
5+
import type { ResourceBinding } from "./auto-update";
6+
import type { RawConfig } from "./config";
7+
8+
// Registry of resource configuration patterns for better maintainability
9+
export const RESOURCE_CONFIG_REGISTRY = {
10+
d1_databases: {
11+
displayName: "D1 Database",
12+
genericBindingName: "DB",
13+
identifierField: "database_id" as const,
14+
createConfig: (resource: ResourceBinding, bindingName: string) => ({
15+
binding: bindingName,
16+
database_name: resource.name,
17+
database_id: resource.id,
18+
}),
19+
},
20+
r2_buckets: {
21+
displayName: "R2 Bucket",
22+
genericBindingName: "BUCKET",
23+
identifierField: "bucket_name" as const,
24+
createConfig: (resource: ResourceBinding, bindingName: string) => ({
25+
binding: bindingName,
26+
bucket_name: resource.name,
27+
}),
28+
},
29+
kv_namespaces: {
30+
displayName: "KV Namespace",
31+
genericBindingName: "KV",
32+
identifierField: "id" as const,
33+
createConfig: (resource: ResourceBinding, bindingName: string) => ({
34+
binding: bindingName,
35+
id: resource.id,
36+
}),
37+
},
38+
vectorize: {
39+
displayName: "Vectorize Index",
40+
genericBindingName: "VECTORIZE",
41+
identifierField: "index_name" as const,
42+
createConfig: (resource: ResourceBinding, bindingName: string) => ({
43+
binding: bindingName,
44+
index_name: resource.name,
45+
}),
46+
},
47+
hyperdrive: {
48+
displayName: "Hyperdrive Configuration",
49+
genericBindingName: "HYPERDRIVE",
50+
identifierField: "id" as const,
51+
createConfig: (resource: ResourceBinding, bindingName: string) => ({
52+
binding: bindingName,
53+
id: resource.id,
54+
}),
55+
},
56+
} as const;
57+
58+
export function createBindingConfig(
59+
resource: ResourceBinding,
60+
bindingName: string
61+
): Record<string, unknown> {
62+
const configTemplate = RESOURCE_CONFIG_REGISTRY[resource.type];
63+
if (!configTemplate) {
64+
throw new Error(`Unsupported resource type: ${resource.type}`);
65+
}
66+
return configTemplate.createConfig(resource, bindingName);
67+
}
68+
69+
export function getBindingIdentifier(
70+
binding: Record<string, unknown>,
71+
type: ResourceBinding["type"]
72+
): string | undefined {
73+
const configTemplate = RESOURCE_CONFIG_REGISTRY[type];
74+
if (!configTemplate) {
75+
return undefined;
76+
}
77+
78+
const field = configTemplate.identifierField;
79+
return binding[field] as string;
80+
}
81+
82+
/**
83+
* Checks if a binding name conflicts with existing bindings across all resource types.
84+
* Binding names are case-insensitive in JavaScript.
85+
*/
86+
export function hasBindingNameConflict(
87+
config: RawConfig,
88+
bindingName: string
89+
): boolean {
90+
const normalizedName = bindingName.toUpperCase();
91+
92+
// Check all resource types for existing bindings
93+
for (const resourceType of Object.keys(RESOURCE_CONFIG_REGISTRY)) {
94+
const bindings = config[
95+
resourceType as keyof typeof RESOURCE_CONFIG_REGISTRY
96+
] as Array<Record<string, unknown>> | undefined;
97+
if (bindings) {
98+
for (const binding of bindings) {
99+
if (binding.binding && typeof binding.binding === "string") {
100+
if (binding.binding.toUpperCase() === normalizedName) {
101+
return true;
102+
}
103+
}
104+
}
105+
}
106+
}
107+
108+
return false;
109+
}
110+
111+
/**
112+
* Gets the display name for a resource type from the registry.
113+
*/
114+
export function getResourceDisplayName(
115+
resourceType: ResourceBinding["type"]
116+
): string {
117+
const config = RESOURCE_CONFIG_REGISTRY[resourceType];
118+
return config?.displayName || resourceType.replace(/_/g, " ");
119+
}
120+
121+
/**
122+
* Gets the generic binding name for a resource type from the registry.
123+
*/
124+
export function getGenericBindingName(
125+
resourceType: ResourceBinding["type"]
126+
): string {
127+
const config = RESOURCE_CONFIG_REGISTRY[resourceType];
128+
return config?.genericBindingName || resourceType.toUpperCase();
129+
}
130+
131+
/**
132+
* Display a configuration snippet for a resource binding.
133+
* This is used to show users how to manually add the binding if auto-update fails.
134+
*/
135+
export function displayConfigSnippet(
136+
resource: ResourceBinding,
137+
configPath: string | undefined,
138+
bindingName?: string
139+
) {
140+
const actualBindingName =
141+
bindingName || resource.binding || getGenericBindingName(resource.type);
142+
const newBinding = createBindingConfig(resource, actualBindingName);
143+
const snippet = { [resource.type]: [newBinding] };
144+
145+
logger.log("\n" + formatConfigSnippet(snippet, configPath));
146+
}
147+
148+
/**
149+
* Validates that a binding name is a valid JavaScript identifier and doesn't conflict with existing bindings.
150+
*/
151+
export function validateBindingName(
152+
config: RawConfig,
153+
bindingName: string
154+
): { valid: boolean; error?: string } {
155+
// Check if it's a valid JavaScript identifier
156+
if (!isValidIdentifier(bindingName)) {
157+
return {
158+
valid: false,
159+
error: `"${bindingName}" is not a valid JavaScript identifier. Binding names must start with a letter, underscore, or $ and contain only letters, numbers, underscores, and $.`,
160+
};
161+
}
162+
163+
// Check for conflicts with existing bindings
164+
if (hasBindingNameConflict(config, bindingName)) {
165+
return {
166+
valid: false,
167+
error: `Binding name "${bindingName}" already exists. Please choose a different name.`,
168+
};
169+
}
170+
171+
return { valid: true };
172+
}
173+
174+
/**
175+
* Asks if the user wants to add the resource to their wrangler config.
176+
*/
177+
export async function promptForConfigUpdate(
178+
resourceType: ResourceBinding["type"]
179+
): Promise<boolean> {
180+
return await confirm(
181+
`Would you like to add this ${getResourceDisplayName(resourceType)} to your wrangler.jsonc?`,
182+
{ defaultValue: true, fallbackValue: false }
183+
);
184+
}
185+
186+
/**
187+
* Prompts the user for a binding name with conflict resolution.
188+
* Shows a placeholder based on the resource type and handles conflicts by re-prompting.
189+
*/
190+
export async function promptForValidBindingName(
191+
config: RawConfig,
192+
resourceType: ResourceBinding["type"],
193+
conflictingName?: string
194+
): Promise<string> {
195+
const placeholder = getGenericBindingName(resourceType);
196+
197+
// If there's a conflicting name, ask if they want to enter a new one
198+
if (conflictingName) {
199+
const shouldTryAgain = await confirm(
200+
`That binding name is not available. Would you like to enter a new binding name?`,
201+
{ defaultValue: true, fallbackValue: false }
202+
);
203+
204+
if (!shouldTryAgain) {
205+
throw new Error(
206+
"Binding name conflict - user chose not to provide alternative"
207+
);
208+
}
209+
}
210+
211+
let bindingName: string = "";
212+
let isValid = false;
213+
let currentDefault: string | undefined = placeholder;
214+
215+
while (!isValid) {
216+
const promptOptions = currentDefault
217+
? { defaultValue: currentDefault }
218+
: {};
219+
220+
bindingName = await prompt(`What binding name would you like to use?`, promptOptions);
221+
222+
// Use currentDefault if user just pressed enter and we have a default
223+
if (!bindingName || bindingName.trim() === "") {
224+
if (currentDefault) {
225+
bindingName = currentDefault;
226+
} else {
227+
// If no default and user pressed enter, continue the loop
228+
continue;
229+
}
230+
}
231+
232+
const validation = validateBindingName(config, bindingName);
233+
if (validation.valid) {
234+
isValid = true;
235+
} else {
236+
logger.error(validation.error || "Invalid binding name");
237+
// Don't show the failed binding name as default on next attempt
238+
currentDefault = undefined;
239+
}
240+
}
241+
242+
return bindingName;
243+
}

0 commit comments

Comments
 (0)