Skip to content

Commit 068bde2

Browse files
MattieTKclaude
andauthored
fix(deploy): upload Build Output modules under their manifest paths (#393)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent db11d01 commit 068bde2

5 files changed

Lines changed: 84 additions & 15 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"cf": patch
3+
---
4+
5+
Fix `cf deploy` for Python Workers with vendored packages
6+
7+
Upload Build Output modules under their manifest paths in `cf deploy`,
8+
`cf workers versions create` and `cf previews deploy` instead of prefixing them
9+
with `./`. Python Workers use module names as file paths without normalising
10+
them, and import vendored packages from the `python_modules/` directory. With
11+
the prefix, the vendored `workers` SDK was not on the import path, so
12+
`from workers import ...` failed with `No module named 'workers'` (10021).

‎AGENTS.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ derives resource identity from the request URL and sends the request to a
415415
cf-spawned Miniflare local-explorer instance over shared persisted state. It
416416
does not read the project's binding declarations.
417417

418+
### Module Workers only
419+
420+
cf supports only ES module Workers. The service-worker format
421+
(`addEventListener("fetch", …)`, no default export) is deprecated
422+
(see [Migrate to module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/))
423+
and unsupported. Do not design for it or flag service-worker regressions in
424+
review; for example, module upload names do not need to match the
425+
`__<sanitised name>` globals Wrangler's service-worker bundles expect.
426+
418427
## Generator Conventions
419428

420429
How the generator turns forge methods into yargs commands. Important

‎packages/cli/src/__tests__/commands/deploy/index.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -951,12 +951,12 @@ describe("cf deploy", () => {
951951

952952
expect(exitCode).toBe(0);
953953
expect(upload.modules).toContain("index.js");
954-
expect(upload.modules).toContain("./lib.cjs");
955-
expect(upload.modules).toContain("./data.bin");
956-
expect(upload.modules).toContain("./text.txt");
957-
expect(upload.modules).toContain("./helper.wasm");
958-
expect(upload.modules).toContain("./config.json");
959-
expect(upload.modules).toContain("./app.py");
954+
expect(upload.modules).toContain("lib.cjs");
955+
expect(upload.modules).toContain("data.bin");
956+
expect(upload.modules).toContain("text.txt");
957+
expect(upload.modules).toContain("helper.wasm");
958+
expect(upload.modules).toContain("config.json");
959+
expect(upload.modules).toContain("app.py");
960960
expect(std.out).toContain("Deployed");
961961
});
962962
});

‎packages/cli/src/__tests__/lib/deploy-input.test.ts‎

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe("transforms Build Output Specification into deploy input", () => {
4747
expect(result.bundleType).toBe("esm");
4848
expect(result.content).toBe("export default {}");
4949
expect(result.modules).toHaveLength(1);
50-
expect(result.modules[0]?.name).toBe("./helper.wasm");
50+
expect(result.modules[0]?.name).toBe("helper.wasm");
5151
expect(result.modules[0]?.type).toBe("compiled-wasm");
5252
expect(Object.keys(result.dependencies)).toHaveLength(2);
5353
});
@@ -83,9 +83,9 @@ describe("transforms Build Output Specification into deploy input", () => {
8383
expect(builtConfig.manifest?.type).toBe("complete");
8484
expect(result.modules).toEqual(
8585
expect.arrayContaining([
86-
expect.objectContaining({ name: "./chunk.mjs", type: "esm" }),
86+
expect.objectContaining({ name: "chunk.mjs", type: "esm" }),
8787
expect.objectContaining({
88-
name: "./helper.wasm",
88+
name: "helper.wasm",
8989
type: "compiled-wasm",
9090
}),
9191
])
@@ -266,11 +266,55 @@ describe("transforms Build Output Specification into deploy input", () => {
266266
const typeMap = Object.fromEntries(
267267
result.modules.map((m) => [m.name, m.type])
268268
);
269-
expect(typeMap["./lib.cjs"]).toBe("commonjs");
270-
expect(typeMap["./app.py"]).toBe("python");
271-
expect(typeMap["./dep.txt"]).toBe("text");
272-
expect(typeMap["./blob.bin"]).toBe("buffer");
273-
expect(typeMap["./config.json"]).toBe("text");
269+
expect(typeMap["lib.cjs"]).toBe("commonjs");
270+
expect(typeMap["app.py"]).toBe("python");
271+
expect(typeMap["dep.txt"]).toBe("text");
272+
expect(typeMap["blob.bin"]).toBe("buffer");
273+
expect(typeMap["config.json"]).toBe("text");
274+
});
275+
276+
it("keeps Python vendored module paths exactly as in the manifest", async () => {
277+
// Python Workers import vendored packages from `python_modules/` and use
278+
// module names as file paths, so a `./` prefix makes `import workers` fail.
279+
await seed({
280+
".cloudflare/output/v0/config.json": buildOutputRootConfig(),
281+
".cloudflare/output/v0/workers/default/worker.config.json": workerConfig({
282+
name: "my-worker",
283+
compatibilityFlags: ["python_workers"],
284+
mainModule: "entry.py",
285+
modules: {
286+
"entry.py": { type: "python" },
287+
"helpers.py": { type: "python" },
288+
"python_modules/workers/__init__.py": { type: "data" },
289+
"python_modules/_workers_sdk_patch_loader.pth": { type: "data" },
290+
"python_modules/workers/workflows.js": { type: "esm" },
291+
},
292+
}),
293+
".cloudflare/output/v0/workers/default/bundle/entry.py":
294+
"from workers import WorkerEntrypoint",
295+
".cloudflare/output/v0/workers/default/bundle/helpers.py": "pass",
296+
".cloudflare/output/v0/workers/default/bundle/python_modules/workers/__init__.py":
297+
"",
298+
".cloudflare/output/v0/workers/default/bundle/python_modules/_workers_sdk_patch_loader.pth":
299+
"",
300+
".cloudflare/output/v0/workers/default/bundle/python_modules/workers/workflows.js":
301+
"export {}",
302+
});
303+
304+
const {
305+
rootConfig,
306+
workers: { default: worker },
307+
} = await readBuildOutput(process.cwd());
308+
const { builtConfig } = parseWorkerConfig(worker, rootConfig);
309+
const result = assembleBuildResult(worker, builtConfig);
310+
311+
expect(result.bundleType).toBe("python");
312+
expect(result.modules.map((m) => m.name).sort()).toEqual([
313+
"helpers.py",
314+
"python_modules/_workers_sdk_patch_loader.pth",
315+
"python_modules/workers/__init__.py",
316+
"python_modules/workers/workflows.js",
317+
]);
274318
});
275319

276320
it("throws when run_worker_first is set without a user Worker", async () => {

‎packages/cli/src/lib/deploy-input.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,12 @@ export function assembleBuildResult(
274274
? readFileSync(filePath)
275275
: readFileSync(filePath, "utf8");
276276

277+
// Use the manifest key unchanged. The runtime normalises `./` in
278+
// JavaScript module names, but Python Workers use module names as file
279+
// paths without normalising them, so a `./python_modules/...` package
280+
// is not on `sys.path`.
277281
cfModules.push({
278-
name: `./${name}`,
282+
name,
279283
filePath,
280284
content: moduleContent,
281285
type: OUTPUT_TO_CF_MODULE_TYPE[meta.type],

0 commit comments

Comments
 (0)