Skip to content

Commit 9d4a32f

Browse files
committed
thomasgauvin: support automatic updates of wrangler.jsonc when creating a new resource in a worker project
1 parent 87e13c0 commit 9d4a32f

File tree

7 files changed

+760
-53
lines changed

7 files changed

+760
-53
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
import fs from "node:fs";
2+
import { beforeEach, describe, expect, test, vi } from "vitest";
3+
import { autoUpdateWranglerConfig } from "../../config/auto-update";
4+
import { findWranglerConfig } from "../../config/config-helpers";
5+
import { configFormat } from "../../config/index";
6+
import { parseJSONC, readFileSync } from "../../parse";
7+
8+
// Mock the logger to avoid console output during tests
9+
vi.mock("../../logger", () => ({
10+
logger: {
11+
log: vi.fn(),
12+
debug: vi.fn(),
13+
},
14+
}));
15+
16+
// Mock dialogs to avoid interactive prompts during tests
17+
vi.mock("../../dialogs", () => ({
18+
confirm: vi.fn().mockResolvedValue(true),
19+
}));
20+
21+
// Mock findWranglerConfig
22+
vi.mock("../../config/config-helpers", () => ({
23+
findWranglerConfig: vi.fn(),
24+
}));
25+
26+
// Mock parse functions
27+
vi.mock("../../parse", () => ({
28+
readFileSync: vi.fn(),
29+
parseJSONC: vi.fn(),
30+
}));
31+
32+
// Mock config format
33+
vi.mock("../../config/index", () => ({
34+
configFormat: vi.fn(),
35+
formatConfigSnippet: vi.fn((snippet: any) =>
36+
JSON.stringify(snippet, null, 2)
37+
),
38+
}));
39+
40+
// Mock getValidBindingName
41+
vi.mock("../../utils/getValidBindingName", () => ({
42+
getValidBindingName: vi.fn((name: string, fallback: string) => {
43+
// Simple mock implementation for testing
44+
return name.toUpperCase().replace(/[^A-Z0-9_]/g, "_") || fallback;
45+
}),
46+
}));
47+
48+
describe("autoUpdateWranglerConfig", () => {
49+
const mockFindWranglerConfig = vi.mocked(findWranglerConfig);
50+
const mockConfigFormat = vi.mocked(configFormat);
51+
const mockReadFileSync = vi.mocked(readFileSync);
52+
const mockParseJSONC = vi.mocked(parseJSONC);
53+
54+
beforeEach(() => {
55+
vi.clearAllMocks();
56+
});
57+
58+
test("should return false when no config file is found", async () => {
59+
mockFindWranglerConfig.mockReturnValue({
60+
configPath: undefined,
61+
userConfigPath: undefined,
62+
});
63+
64+
const result = await autoUpdateWranglerConfig({
65+
type: "d1_databases",
66+
id: "test-id",
67+
name: "test-db",
68+
});
69+
70+
expect(result).toBe(false);
71+
});
72+
73+
test("should return false for unsupported config format (TOML)", async () => {
74+
mockFindWranglerConfig.mockReturnValue({
75+
configPath: "/path/to/wrangler.toml",
76+
userConfigPath: "/path/to/wrangler.toml",
77+
});
78+
mockConfigFormat.mockReturnValue("toml");
79+
80+
const result = await autoUpdateWranglerConfig({
81+
type: "d1_databases",
82+
id: "test-id",
83+
name: "test-db",
84+
});
85+
86+
expect(result).toBe(false);
87+
});
88+
89+
test("should create proper D1 binding configuration with auto-update", async () => {
90+
const configPath = "/tmp/test-wrangler.json";
91+
const initialConfig = {
92+
name: "my-worker",
93+
compatibility_date: "2023-01-01",
94+
};
95+
96+
// Mock file system operations
97+
mockReadFileSync.mockReturnValue(JSON.stringify(initialConfig));
98+
mockParseJSONC.mockReturnValue(initialConfig);
99+
mockConfigFormat.mockReturnValue("jsonc");
100+
vi.spyOn(fs, "writeFileSync").mockImplementation(() => {});
101+
102+
mockFindWranglerConfig.mockReturnValue({
103+
configPath,
104+
userConfigPath: configPath,
105+
});
106+
107+
const result = await autoUpdateWranglerConfig(
108+
{
109+
type: "d1_databases",
110+
id: "test-db-id",
111+
name: "test-db",
112+
},
113+
true
114+
); // auto-update enabled
115+
116+
expect(result).toBe(true);
117+
expect(fs.writeFileSync).toHaveBeenCalledWith(
118+
configPath,
119+
expect.stringContaining('"d1_databases"')
120+
);
121+
});
122+
123+
test("should create proper R2 binding configuration with auto-update", async () => {
124+
const configPath = "/tmp/test-wrangler.json";
125+
const initialConfig = {
126+
name: "my-worker",
127+
};
128+
129+
mockReadFileSync.mockReturnValue(JSON.stringify(initialConfig));
130+
mockParseJSONC.mockReturnValue(initialConfig);
131+
mockConfigFormat.mockReturnValue("jsonc");
132+
vi.spyOn(fs, "writeFileSync").mockImplementation(() => {});
133+
134+
mockFindWranglerConfig.mockReturnValue({
135+
configPath,
136+
userConfigPath: configPath,
137+
});
138+
139+
const result = await autoUpdateWranglerConfig(
140+
{
141+
type: "r2_buckets",
142+
id: "test-bucket",
143+
name: "test-bucket",
144+
},
145+
true
146+
); // auto-update enabled
147+
148+
expect(result).toBe(true);
149+
expect(fs.writeFileSync).toHaveBeenCalledWith(
150+
configPath,
151+
expect.stringContaining('"r2_buckets"')
152+
);
153+
});
154+
155+
test("should not add duplicate bindings", async () => {
156+
const configPath = "/tmp/test-wrangler.json";
157+
const initialConfig = {
158+
name: "my-worker",
159+
d1_databases: [
160+
{
161+
binding: "DB",
162+
database_name: "test-db",
163+
database_id: "test-db-id",
164+
},
165+
],
166+
};
167+
168+
mockReadFileSync.mockReturnValue(JSON.stringify(initialConfig));
169+
mockParseJSONC.mockReturnValue(initialConfig);
170+
mockConfigFormat.mockReturnValue("jsonc");
171+
vi.spyOn(fs, "writeFileSync").mockImplementation(() => {});
172+
173+
mockFindWranglerConfig.mockReturnValue({
174+
configPath,
175+
userConfigPath: configPath,
176+
});
177+
178+
const result = await autoUpdateWranglerConfig(
179+
{
180+
type: "d1_databases",
181+
id: "test-db-id",
182+
name: "test-db",
183+
},
184+
true
185+
); // auto-update enabled
186+
187+
expect(result).toBe(false);
188+
// Should not call writeFileSync since binding already exists
189+
expect(fs.writeFileSync).not.toHaveBeenCalled();
190+
});
191+
192+
test("should generate unique binding name when conflicts exist", async () => {
193+
const configPath = "/tmp/test-wrangler.json";
194+
const initialConfig = {
195+
name: "my-worker",
196+
d1_databases: [
197+
{
198+
binding: "DB",
199+
database_name: "existing-db",
200+
database_id: "existing-db-id",
201+
},
202+
],
203+
r2_buckets: [
204+
{
205+
binding: "DB_1",
206+
bucket_name: "existing-bucket",
207+
},
208+
],
209+
};
210+
211+
mockReadFileSync.mockReturnValue(JSON.stringify(initialConfig));
212+
mockParseJSONC.mockReturnValue(initialConfig);
213+
mockConfigFormat.mockReturnValue("jsonc");
214+
const writeFileSyncSpy = vi
215+
.spyOn(fs, "writeFileSync")
216+
.mockImplementation(() => {});
217+
218+
mockFindWranglerConfig.mockReturnValue({
219+
configPath,
220+
userConfigPath: configPath,
221+
});
222+
223+
const result = await autoUpdateWranglerConfig(
224+
{
225+
type: "d1_databases",
226+
id: "new-test-db-id",
227+
name: "test-db", // Will use generic name "DB"
228+
},
229+
true
230+
); // auto-update enabled
231+
232+
expect(result).toBe(true);
233+
expect(writeFileSyncSpy).toHaveBeenCalledWith(
234+
configPath,
235+
expect.stringContaining('"DB_2"') // Should generate DB_2 to avoid conflicts with DB and DB_1
236+
);
237+
});
238+
239+
test("should handle case-insensitive binding name conflicts", async () => {
240+
const configPath = "/tmp/test-wrangler.json";
241+
const initialConfig = {
242+
name: "my-worker",
243+
d1_databases: [
244+
{
245+
binding: "bucket", // lowercase to conflict with R2's "BUCKET"
246+
database_name: "existing-db",
247+
database_id: "existing-db-id",
248+
},
249+
],
250+
};
251+
252+
mockReadFileSync.mockReturnValue(JSON.stringify(initialConfig));
253+
mockParseJSONC.mockReturnValue(initialConfig);
254+
mockConfigFormat.mockReturnValue("jsonc");
255+
const writeFileSyncSpy = vi
256+
.spyOn(fs, "writeFileSync")
257+
.mockImplementation(() => {});
258+
259+
mockFindWranglerConfig.mockReturnValue({
260+
configPath,
261+
userConfigPath: configPath,
262+
});
263+
264+
const result = await autoUpdateWranglerConfig(
265+
{
266+
type: "r2_buckets",
267+
id: "new-bucket-id",
268+
name: "test-bucket", // Will use generic name "BUCKET" but conflicts with "bucket"
269+
},
270+
true
271+
); // auto-update enabled
272+
273+
expect(result).toBe(true);
274+
expect(writeFileSyncSpy).toHaveBeenCalledWith(
275+
configPath,
276+
expect.stringContaining('"BUCKET_1"') // Should generate BUCKET_1 due to case-insensitive conflict
277+
);
278+
});
279+
280+
test("should not auto-update for KV preview namespaces", async () => {
281+
// This test documents that KV preview namespaces should be handled
282+
// differently in the KV create command, not in the auto-update module
283+
284+
// Mock no config file found
285+
mockFindWranglerConfig.mockReturnValue({
286+
configPath: undefined,
287+
userConfigPath: undefined,
288+
});
289+
290+
const result = await autoUpdateWranglerConfig(
291+
{
292+
type: "kv_namespaces",
293+
id: "preview-kv-id",
294+
name: "preview-namespace",
295+
additionalConfig: { preview_id: "preview-kv-id" },
296+
},
297+
true
298+
);
299+
300+
// The auto-update module itself doesn't distinguish preview vs regular
301+
// The KV command should skip calling auto-update for preview namespaces
302+
expect(result).toBe(false); // No config file found, so returns false
303+
});
304+
305+
test("should respect user decline when prompting for update", async () => {
306+
const { confirm } = await import("../../dialogs");
307+
vi.mocked(confirm).mockResolvedValueOnce(false);
308+
309+
const configPath = "/tmp/test-wrangler.json";
310+
const initialConfig = { name: "my-worker" };
311+
312+
mockReadFileSync.mockReturnValue(JSON.stringify(initialConfig));
313+
mockParseJSONC.mockReturnValue(initialConfig);
314+
mockConfigFormat.mockReturnValue("jsonc");
315+
vi.spyOn(fs, "writeFileSync").mockImplementation(() => {});
316+
317+
mockFindWranglerConfig.mockReturnValue({
318+
configPath,
319+
userConfigPath: configPath,
320+
});
321+
322+
const result = await autoUpdateWranglerConfig(
323+
{
324+
type: "d1_databases",
325+
id: "test-db-id",
326+
name: "test-db",
327+
},
328+
false
329+
); // auto-update disabled, should prompt
330+
331+
expect(result).toBe(false);
332+
expect(fs.writeFileSync).not.toHaveBeenCalled();
333+
expect(confirm).toHaveBeenCalledWith(
334+
expect.stringContaining(
335+
"Would you like to update the wrangler.jsonc file"
336+
),
337+
{ defaultValue: true, fallbackValue: false }
338+
);
339+
});
340+
});

0 commit comments

Comments
 (0)