Skip to content

Commit 7ea69af

Browse files
authored
fix: support function-based Vite configs in autoconfig setup (#12562)
1 parent 33a9a8f commit 7ea69af

File tree

3 files changed

+273
-15
lines changed

3 files changed

+273
-15
lines changed

.changeset/function-vite-config.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Support function-based Vite configs in autoconfig setup
6+
7+
`wrangler setup` and `wrangler deploy --x-autoconfig` can now automatically add the Cloudflare Vite plugin to projects that use function-based `defineConfig()` patterns. Previously, autoconfig would fail with "Cannot modify Vite config: expected an object literal but found ArrowFunctionExpression" for configs like:
8+
9+
```ts
10+
export default defineConfig(({ isSsrBuild }) => ({
11+
plugins: [reactRouter(), tsconfigPaths()],
12+
}));
13+
```
14+
15+
This pattern is used by several official framework templates, including React Router's `node-postgres` and `node-custom-server` templates. The following `defineConfig()` patterns are now supported:
16+
17+
- `defineConfig({ ... })` (object literal, already worked)
18+
- `defineConfig(() => ({ ... }))` (arrow function with expression body)
19+
- `defineConfig(({ isSsrBuild }) => ({ ... }))` (arrow function with destructured params)
20+
- `defineConfig(() => { return { ... }; })` (arrow function with block body)
21+
- `defineConfig(function() { return { ... }; })` (function expression)

packages/wrangler/src/__tests__/autoconfig/vite-config.test.ts

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { readFileSync } from "node:fs";
12
import { writeFile } from "node:fs/promises";
23
import { beforeEach, describe, it } from "vitest";
34
import {
@@ -17,7 +18,26 @@ describe("vite-config utils", () => {
1718
});
1819

1920
describe("checkIfViteConfigUsesCloudflarePlugin", () => {
20-
it("should handle vite config with function-based defineConfig", async ({
21+
it("should detect cloudflare plugin in function-based defineConfig", async ({
22+
expect,
23+
}) => {
24+
await writeFile(
25+
"vite.config.ts",
26+
`
27+
import { defineConfig } from 'vite';
28+
import { cloudflare } from '@cloudflare/vite-plugin';
29+
30+
export default defineConfig(() => ({
31+
plugins: [cloudflare()]
32+
}));
33+
`
34+
);
35+
36+
const result = checkIfViteConfigUsesCloudflarePlugin(".");
37+
expect(result).toBe(true);
38+
});
39+
40+
it("should return false for function-based defineConfig without cloudflare plugin", async ({
2141
expect,
2242
}) => {
2343
await writeFile(
@@ -33,7 +53,6 @@ export default defineConfig(() => ({
3353

3454
const result = checkIfViteConfigUsesCloudflarePlugin(".");
3555
expect(result).toBe(false);
36-
expect(std.debug).toContain("Vite config uses a non-object expression");
3756
});
3857

3958
it("should handle vite config without plugins array", async ({
@@ -76,7 +95,7 @@ export default defineConfig({
7695
});
7796

7897
describe("transformViteConfig", () => {
79-
it("should throw UserError for function-based defineConfig", async ({
98+
it("should successfully transform function-based defineConfig with arrow expression body", async ({
8099
expect,
81100
}) => {
82101
await writeFile(
@@ -90,9 +109,142 @@ export default defineConfig(() => ({
90109
`
91110
);
92111

93-
expect(() => transformViteConfig(".")).toThrowError(
94-
/Cannot modify Vite config: expected an object literal/
112+
expect(() => transformViteConfig(".")).not.toThrow();
113+
const result = readFileSync("vite.config.ts", "utf-8");
114+
expect(result).toContain(
115+
'import { cloudflare } from "@cloudflare/vite-plugin"'
116+
);
117+
expect(result).toContain("cloudflare()");
118+
});
119+
120+
it("should successfully transform function-based defineConfig with destructured params", async ({
121+
expect,
122+
}) => {
123+
// This is the exact pattern from the React Router node-postgres template
124+
await writeFile(
125+
"vite.config.ts",
126+
`
127+
import { reactRouter } from "@react-router/dev/vite";
128+
import tailwindcss from "@tailwindcss/vite";
129+
import { defineConfig } from "vite";
130+
import tsconfigPaths from "vite-tsconfig-paths";
131+
132+
export default defineConfig(({ isSsrBuild }) => ({
133+
build: {
134+
rollupOptions: isSsrBuild
135+
? {
136+
input: "./server/app.ts",
137+
}
138+
: undefined,
139+
},
140+
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
141+
}));
142+
`
143+
);
144+
145+
expect(() => transformViteConfig(".")).not.toThrow();
146+
const result = readFileSync("vite.config.ts", "utf-8");
147+
expect(result).toContain(
148+
'import { cloudflare } from "@cloudflare/vite-plugin"'
149+
);
150+
expect(result).toContain("cloudflare()");
151+
// The existing plugins should be preserved
152+
expect(result).toContain("tailwindcss()");
153+
expect(result).toContain("reactRouter()");
154+
expect(result).toContain("tsconfigPaths()");
155+
// The function structure should be preserved
156+
expect(result).toContain("isSsrBuild");
157+
});
158+
159+
it("should successfully transform function-based defineConfig with block body", async ({
160+
expect,
161+
}) => {
162+
await writeFile(
163+
"vite.config.ts",
164+
`
165+
import { defineConfig } from 'vite';
166+
167+
export default defineConfig(() => {
168+
return {
169+
plugins: []
170+
};
171+
});
172+
`
173+
);
174+
175+
expect(() => transformViteConfig(".")).not.toThrow();
176+
const result = readFileSync("vite.config.ts", "utf-8");
177+
expect(result).toContain(
178+
'import { cloudflare } from "@cloudflare/vite-plugin"'
179+
);
180+
expect(result).toContain("cloudflare()");
181+
});
182+
183+
it("should successfully transform function expression defineConfig", async ({
184+
expect,
185+
}) => {
186+
await writeFile(
187+
"vite.config.ts",
188+
`
189+
import { defineConfig } from 'vite';
190+
191+
export default defineConfig(function() {
192+
return {
193+
plugins: []
194+
};
195+
});
196+
`
197+
);
198+
199+
expect(() => transformViteConfig(".")).not.toThrow();
200+
const result = readFileSync("vite.config.ts", "utf-8");
201+
expect(result).toContain(
202+
'import { cloudflare } from "@cloudflare/vite-plugin"'
203+
);
204+
expect(result).toContain("cloudflare()");
205+
});
206+
207+
it("should pass viteEnvironmentName option with function-based config", async ({
208+
expect,
209+
}) => {
210+
await writeFile(
211+
"vite.config.ts",
212+
`
213+
import { defineConfig } from 'vite';
214+
215+
export default defineConfig(() => ({
216+
plugins: []
217+
}));
218+
`
219+
);
220+
221+
expect(() =>
222+
transformViteConfig(".", { viteEnvironmentName: "ssr" })
223+
).not.toThrow();
224+
const result = readFileSync("vite.config.ts", "utf-8");
225+
expect(result).toContain("viteEnvironment");
226+
expect(result).toContain('"ssr"');
227+
});
228+
229+
it("should remove incompatible plugins from function-based config", async ({
230+
expect,
231+
}) => {
232+
await writeFile(
233+
"vite.config.ts",
234+
`
235+
import { defineConfig } from 'vite';
236+
237+
export default defineConfig(() => ({
238+
plugins: [nitro(), someOther()]
239+
}));
240+
`
95241
);
242+
243+
expect(() => transformViteConfig(".")).not.toThrow();
244+
const result = readFileSync("vite.config.ts", "utf-8");
245+
expect(result).not.toContain("nitro()");
246+
expect(result).toContain("someOther()");
247+
expect(result).toContain("cloudflare()");
96248
});
97249

98250
it("should throw UserError when plugins array is missing", async ({
@@ -114,6 +266,25 @@ export default defineConfig({
114266
);
115267
});
116268

269+
it("should throw UserError when plugins array is missing in function-based config", async ({
270+
expect,
271+
}) => {
272+
await writeFile(
273+
"vite.config.ts",
274+
`
275+
import { defineConfig } from 'vite';
276+
277+
export default defineConfig(() => ({
278+
server: { port: 3000 }
279+
}));
280+
`
281+
);
282+
283+
expect(() => transformViteConfig(".")).toThrowError(
284+
/Cannot modify Vite config: could not find a valid plugins array/
285+
);
286+
});
287+
117288
it("should successfully transform valid vite config", async ({
118289
expect,
119290
}) => {

packages/wrangler/src/autoconfig/frameworks/utils/vite-config.ts

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,57 @@ import type { types } from "recast";
1010
const b = recast.types.builders;
1111
const t = recast.types.namedTypes;
1212

13+
/**
14+
* Extracts the ObjectExpression from the first argument of defineConfig().
15+
*
16+
* Handles:
17+
* - defineConfig({ ... })
18+
* - defineConfig(() => ({ ... }))
19+
* - defineConfig(({ isSsrBuild }) => ({ ... }))
20+
* - defineConfig(() => { return { ... }; })
21+
* - defineConfig(function() { return { ... }; })
22+
*/
23+
function extractConfigObject(
24+
node: recast.types.ASTNode
25+
): types.namedTypes.ObjectExpression | null {
26+
// Direct object: defineConfig({ ... })
27+
if (t.ObjectExpression.check(node)) {
28+
return node;
29+
}
30+
31+
// Arrow function: defineConfig(() => ({ ... })) or defineConfig(() => { return { ... }; })
32+
if (t.ArrowFunctionExpression.check(node)) {
33+
if (t.ObjectExpression.check(node.body)) {
34+
return node.body;
35+
}
36+
if (t.BlockStatement.check(node.body)) {
37+
return extractFromBlockStatement(node.body);
38+
}
39+
}
40+
41+
// Function expression: defineConfig(function() { return { ... }; })
42+
if (t.FunctionExpression.check(node)) {
43+
return extractFromBlockStatement(node.body);
44+
}
45+
46+
return null;
47+
}
48+
49+
function extractFromBlockStatement(
50+
block: types.namedTypes.BlockStatement
51+
): types.namedTypes.ObjectExpression | null {
52+
const returnStmt = block.body.find((s) => t.ReturnStatement.check(s));
53+
if (
54+
returnStmt &&
55+
t.ReturnStatement.check(returnStmt) &&
56+
returnStmt.argument &&
57+
t.ObjectExpression.check(returnStmt.argument)
58+
) {
59+
return returnStmt.argument;
60+
}
61+
return null;
62+
}
63+
1364
export function checkIfViteConfigUsesCloudflarePlugin(
1465
projectPath: string
1566
): boolean {
@@ -39,15 +90,17 @@ export function checkIfViteConfigUsesCloudflarePlugin(
3990
return this.traverse(n);
4091
}
4192

42-
const config = n.node.arguments[0];
43-
if (!t.ObjectExpression.check(config)) {
93+
const configObject = extractConfigObject(n.node.arguments[0]);
94+
if (!configObject) {
4495
logger.debug(
45-
`Vite config uses a non-object expression (e.g., function). Skipping Cloudflare plugin check.`
96+
`Vite config uses an unsupported expression type. Skipping Cloudflare plugin check.`
4697
);
4798
return this.traverse(n);
4899
}
49100

50-
const pluginsProp = config.properties.find((prop) => isPluginsProp(prop));
101+
const pluginsProp = configObject.properties.find((prop) =>
102+
isPluginsProp(prop)
103+
);
51104
if (!pluginsProp || !t.ArrayExpression.check(pluginsProp.value)) {
52105
logger.debug(
53106
`Vite config does not have a valid plugins array. Skipping Cloudflare plugin check.`
@@ -139,18 +192,29 @@ export function transformViteConfig(
139192
// defineConfig({
140193
// plugins: [cloudflare({ viteEnvironment: { name: 'ssr' } })],
141194
// });
195+
// ```
196+
// Also handles function-based configs like:
197+
// ```
198+
// defineConfig(({ isSsrBuild }) => ({
199+
// plugins: [...]
200+
// }));
201+
// ```
142202
const callee = n.node.callee as types.namedTypes.Identifier;
143203
if (callee.name !== "defineConfig") {
144204
return this.traverse(n);
145205
}
146206

147-
const config = n.node.arguments[0];
148-
if (!t.ObjectExpression.check(config)) {
207+
const configObject = extractConfigObject(n.node.arguments[0]);
208+
if (!configObject) {
209+
const argType = n.node.arguments[0]?.type ?? "unknown";
149210
throw new UserError(dedent`
150-
Cannot modify Vite config: expected an object literal but found ${config.type}.
211+
Cannot modify Vite config: could not extract a config object (found ${argType}).
151212
152-
The Cloudflare plugin can only be automatically added to Vite configs that use a simple object.
153-
If your config uses a function or other dynamic configuration, please manually add the plugin:
213+
The Cloudflare plugin can only be automatically added to Vite configs that use:
214+
- A simple object: defineConfig({ plugins: [...] })
215+
- An arrow function returning an object: defineConfig(() => ({ plugins: [...] }))
216+
217+
If your config uses a more complex pattern, please manually add the plugin:
154218
155219
import { cloudflare } from "@cloudflare/vite-plugin";
156220
@@ -160,7 +224,9 @@ export function transformViteConfig(
160224
`);
161225
}
162226

163-
const pluginsProp = config.properties.find((prop) => isPluginsProp(prop));
227+
const pluginsProp = configObject.properties.find((prop) =>
228+
isPluginsProp(prop)
229+
);
164230
if (!pluginsProp || !t.ArrayExpression.check(pluginsProp.value)) {
165231
throw new UserError(dedent`
166232
Cannot modify Vite config: could not find a valid plugins array.

0 commit comments

Comments
 (0)