Skip to content

Commit 5bf2528

Browse files
committed
fix: normalize inline config
1 parent b800775 commit 5bf2528

2 files changed

Lines changed: 227 additions & 52 deletions

File tree

packages/wrangler/e2e/create-server.test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,129 @@ describe("createServer", { sequential: true }, () => {
244244
await expect(response.text()).resolves.toBe("Hello from inline config");
245245
});
246246

247+
it("loads default .env files for config path workers", async ({ expect }) => {
248+
await helper.seed({
249+
"wrangler.jsonc": dedent`
250+
{
251+
"name": "env-worker",
252+
"main": "src/index.ts",
253+
"compatibility_date": "2026-05-20",
254+
"vars": { "CONFIG_VAR": "from-config" }
255+
}
256+
`,
257+
".env": dedent`
258+
ENV_SECRET=from-env
259+
`,
260+
"src/index.ts": dedent`
261+
export default {
262+
fetch(request, env) {
263+
return Response.json({
264+
CONFIG_VAR: env.CONFIG_VAR,
265+
ENV_SECRET: env.ENV_SECRET,
266+
});
267+
}
268+
};
269+
`,
270+
});
271+
272+
const server = createServer({
273+
root: helper.tmpPath,
274+
workers: [{ configPath: "./wrangler.jsonc" }],
275+
});
276+
onTestFinished(server.close);
277+
278+
await server.listen();
279+
280+
const response = await server.fetch("/");
281+
await expect(response.json()).resolves.toEqual({
282+
CONFIG_VAR: "from-config",
283+
ENV_SECRET: "from-env",
284+
});
285+
});
286+
287+
it("loads default .dev.vars files for config path workers", async ({
288+
expect,
289+
}) => {
290+
await helper.seed({
291+
"wrangler.jsonc": dedent`
292+
{
293+
"name": "dev-vars-worker",
294+
"main": "src/index.ts",
295+
"compatibility_date": "2026-05-20",
296+
"vars": { "CONFIG_VAR": "from-config" }
297+
}
298+
`,
299+
".env": dedent`
300+
SECRET=from-env
301+
`,
302+
".dev.vars": dedent`
303+
SECRET=from-dev-vars
304+
`,
305+
"src/index.ts": dedent`
306+
export default {
307+
fetch(request, env) {
308+
return Response.json({
309+
CONFIG_VAR: env.CONFIG_VAR,
310+
SECRET: env.SECRET,
311+
});
312+
}
313+
};
314+
`,
315+
});
316+
317+
const server = createServer({
318+
root: helper.tmpPath,
319+
workers: [{ configPath: "./wrangler.jsonc" }],
320+
});
321+
onTestFinished(server.close);
322+
323+
await server.listen();
324+
325+
const response = await server.fetch("/");
326+
await expect(response.json()).resolves.toEqual({
327+
CONFIG_VAR: "from-config",
328+
SECRET: "from-dev-vars",
329+
});
330+
});
331+
332+
it("supports Workers Sites", async ({ expect }) => {
333+
await helper.seed({
334+
"public/hello.txt": "Hello from Workers Sites",
335+
"src/index.ts": dedent`
336+
import manifestJSON from "__STATIC_CONTENT_MANIFEST";
337+
338+
const manifest = JSON.parse(manifestJSON);
339+
340+
export default {
341+
async fetch(request, env) {
342+
const key = manifest[new URL(request.url).pathname.slice(1)];
343+
const value = key ? await env.__STATIC_CONTENT.get(key) : null;
344+
return new Response(value ?? "missing");
345+
}
346+
};
347+
`,
348+
});
349+
350+
const server = createServer({
351+
root: helper.tmpPath,
352+
workers: [
353+
{
354+
config: {
355+
main: "src/index.ts",
356+
compatibility_date: "2026-05-20",
357+
site: { bucket: "public" },
358+
},
359+
},
360+
],
361+
});
362+
onTestFinished(server.close);
363+
364+
await server.listen();
365+
366+
const response = await server.fetch("/hello.txt");
367+
await expect(response.text()).resolves.toBe("Hello from Workers Sites");
368+
});
369+
247370
it("uses ephemeral storage by default", async ({ expect }) => {
248371
await helper.seed({
249372
"wrangler.jsonc": dedent`

packages/wrangler/src/api/server.ts

Lines changed: 104 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import assert from "node:assert";
22
import path from "node:path";
33
import { fileURLToPath } from "node:url";
4+
import {
5+
normalizeAndValidateConfig,
6+
UserError,
7+
} from "@cloudflare/workers-utils";
48
import { Headers, Request } from "miniflare";
9+
import { validateNodeCompatMode } from "../deployment-bundle/node-compat";
510
import { logger } from "../logger";
11+
import { getSiteAssetPaths } from "../sites";
612
import { requireApiToken, requireAuth } from "../user";
713
import { DevEnv } from "./startDevWorker/DevEnv";
814
import { MultiworkerRuntimeController } from "./startDevWorker/MultiworkerRuntimeController";
@@ -18,9 +24,11 @@ import type {
1824
FetcherScheduledOptions,
1925
FetcherScheduledResult,
2026
} from "@cloudflare/workers-types/experimental";
21-
import type { RawEnvironment } from "@cloudflare/workers-utils";
27+
import type { Config, RawConfig } from "@cloudflare/workers-utils";
2228
import type { DispatchFetch, RequestInfo } from "miniflare";
2329

30+
export type InlineConfig = Omit<RawConfig, "env">;
31+
2432
export type WorkerInput =
2533
| {
2634
root?: string;
@@ -29,7 +37,7 @@ export type WorkerInput =
2937
}
3038
| {
3139
root?: string;
32-
config: RawEnvironment;
40+
config: InlineConfig;
3341
};
3442

3543
type DevServerOptions = Exclude<
@@ -92,6 +100,31 @@ function resolvePath(basePath: string, maybePath: string | URL): string {
92100
: path.resolve(basePath, maybePath);
93101
}
94102

103+
function normalizeInlineWorkerConfig(
104+
config: InlineConfig,
105+
root: string
106+
): Config {
107+
const configPath = path.join(root, "wrangler.jsonc");
108+
const { config: normalizedConfig, diagnostics } = normalizeAndValidateConfig(
109+
config,
110+
configPath,
111+
configPath,
112+
{}
113+
);
114+
115+
if (diagnostics.hasWarnings()) {
116+
logger.warn(diagnostics.renderWarnings());
117+
}
118+
119+
if (diagnostics.hasErrors()) {
120+
throw new UserError(diagnostics.renderErrors(), {
121+
telemetryMessage: "create server inline config validation failed",
122+
});
123+
}
124+
125+
return normalizedConfig;
126+
}
127+
95128
async function resolveFetchInput(
96129
input: RequestInfo,
97130
session: ServerSession
@@ -128,60 +161,79 @@ function resolveWorkerInputs(
128161
return options.workers.map((input, index, list) => {
129162
const isPrimaryWorker = index === 0;
130163
const isMultiworker = list.length > 1;
131-
const dev: StartDevWorkerInput["dev"] = {
132-
auth,
133-
remote: options.allowRemoteBindings ? undefined : false,
134-
server: options.server ?? { hostname: "127.0.0.1", port: 0 },
135-
logLevel: options.logLevel ?? "error",
136-
watch: options.watch ?? false,
137-
persist:
138-
options.persist === true ? undefined : (options.persist ?? false),
139-
inspector: options.inspector ?? false,
140-
outboundService:
141-
options.outboundService ??
142-
((request) => {
143-
return globalThis.fetch(request.url, request);
144-
}),
145-
multiworkerPrimary: isPrimaryWorker && isMultiworker ? true : undefined,
146-
};
147164
const root = input.root ?? options.root ?? cwd;
165+
const inlineConfig =
166+
"config" in input
167+
? normalizeInlineWorkerConfig(input.config, root)
168+
: undefined;
148169

149-
if ("config" in input) {
150-
const config = input.config;
151-
152-
return {
153-
// FIXME: to avoid dev env from auto discovering a config file and merging it with the inline config
154-
config: "",
155-
name: config.name,
156-
entrypoint: config.main ? resolvePath(root, config.main) : undefined,
157-
compatibilityDate: config.compatibility_date,
158-
compatibilityFlags: config.compatibility_flags,
159-
complianceRegion: config.compliance_region,
160-
bindings: convertConfigToBindings(config, { usePreviewIds: true }),
161-
migrations: config.migrations,
162-
containers: config.containers,
163-
triggers: config.triggers?.crons?.map((cron) => ({
164-
type: "cron",
165-
cron,
166-
})),
167-
tailConsumers: config.tail_consumers,
168-
streamingTailConsumers: config.streaming_tail_consumers,
169-
assets: config.assets?.directory,
170-
dev,
171-
};
172-
}
170+
return {
171+
// Uses an empty string to avoid dev env from auto discovering a config file and merging it with the inline config
172+
config: "configPath" in input ? resolvePath(root, input.configPath) : "",
173+
env: "configPath" in input ? input.env : undefined,
174+
name: inlineConfig?.name,
175+
entrypoint: inlineConfig?.main,
176+
compatibilityDate: inlineConfig?.compatibility_date,
177+
compatibilityFlags: inlineConfig?.compatibility_flags,
178+
complianceRegion: inlineConfig?.compliance_region,
179+
pythonModules: inlineConfig?.python_modules,
180+
bindings: inlineConfig
181+
? convertConfigToBindings(inlineConfig, { usePreviewIds: true })
182+
: undefined,
183+
migrations: inlineConfig?.migrations,
184+
containers: inlineConfig?.containers,
185+
triggers: inlineConfig?.triggers?.crons?.map((cron) => ({
186+
type: "cron" as const,
187+
cron,
188+
})),
189+
tailConsumers: inlineConfig?.tail_consumers,
190+
streamingTailConsumers: inlineConfig?.streaming_tail_consumers,
191+
assets: inlineConfig?.assets?.directory,
192+
dev: {
193+
auth,
194+
remote: options.allowRemoteBindings ? undefined : false,
195+
server: options.server ?? { hostname: "127.0.0.1", port: 0 },
196+
logLevel: options.logLevel ?? "error",
197+
watch: options.watch ?? false,
198+
persist:
199+
options.persist === true ? undefined : (options.persist ?? false),
200+
inspector: options.inspector ?? false,
201+
outboundService:
202+
options.outboundService ??
203+
((request) => {
204+
return globalThis.fetch(request.url, request);
205+
}),
206+
multiworkerPrimary: isPrimaryWorker && isMultiworker ? true : undefined,
207+
},
208+
build: {
209+
nodejsCompatMode: (config) => {
210+
const hookConfig = inlineConfig ?? config;
211+
return validateNodeCompatMode(
212+
hookConfig.compatibility_date,
213+
hookConfig.compatibility_flags ?? [],
214+
{ noBundle: hookConfig.no_bundle }
215+
);
216+
},
217+
},
218+
legacy: {
219+
site: (config) => {
220+
const legacyAssetPaths = getSiteAssetPaths(inlineConfig ?? config);
173221

174-
if ("configPath" in input) {
175-
return {
176-
config: resolvePath(root, input.configPath),
177-
env: input.env,
178-
dev,
179-
};
180-
}
222+
if (!legacyAssetPaths) {
223+
return undefined;
224+
}
181225

182-
throw new Error(
183-
`Invalid worker input at index ${index}. Expected an object with either a "config" property or a "configPath" property.`
184-
);
226+
return {
227+
bucket: path.join(
228+
legacyAssetPaths.baseDirectory,
229+
legacyAssetPaths.assetDirectory
230+
),
231+
include: legacyAssetPaths.includePatterns,
232+
exclude: legacyAssetPaths.excludePatterns,
233+
};
234+
},
235+
},
236+
};
185237
});
186238
}
187239

0 commit comments

Comments
 (0)