diff --git a/.changeset/python-vendored-module-names.md b/.changeset/python-vendored-module-names.md new file mode 100644 index 0000000000..f6c43e3dae --- /dev/null +++ b/.changeset/python-vendored-module-names.md @@ -0,0 +1,12 @@ +--- +"cf": patch +--- + +Fix `cf deploy` for Python Workers with vendored packages + +Upload Build Output modules under their manifest paths in `cf deploy`, +`cf workers versions create` and `cf previews deploy` instead of prefixing them +with `./`. Python Workers use module names as file paths without normalising +them, and import vendored packages from the `python_modules/` directory. With +the prefix, the vendored `workers` SDK was not on the import path, so +`from workers import ...` failed with `No module named 'workers'` (10021). diff --git a/AGENTS.md b/AGENTS.md index 04fc82f0a7..a8d2a7338b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -415,6 +415,15 @@ derives resource identity from the request URL and sends the request to a cf-spawned Miniflare local-explorer instance over shared persisted state. It does not read the project's binding declarations. +### Module Workers only + +cf supports only ES module Workers. The service-worker format +(`addEventListener("fetch", …)`, no default export) is deprecated +(see [Migrate to module Workers](https://developers.cloudflare.com/workers/reference/migrate-to-module-workers/)) +and unsupported. Do not design for it or flag service-worker regressions in +review; for example, module upload names do not need to match the +`__` globals Wrangler's service-worker bundles expect. + ## Generator Conventions How the generator turns forge methods into yargs commands. Important diff --git a/packages/cli/src/__tests__/commands/deploy/index.test.ts b/packages/cli/src/__tests__/commands/deploy/index.test.ts index 07e41a6a26..1d26d77831 100644 --- a/packages/cli/src/__tests__/commands/deploy/index.test.ts +++ b/packages/cli/src/__tests__/commands/deploy/index.test.ts @@ -951,12 +951,12 @@ describe("cf deploy", () => { expect(exitCode).toBe(0); expect(upload.modules).toContain("index.js"); - expect(upload.modules).toContain("./lib.cjs"); - expect(upload.modules).toContain("./data.bin"); - expect(upload.modules).toContain("./text.txt"); - expect(upload.modules).toContain("./helper.wasm"); - expect(upload.modules).toContain("./config.json"); - expect(upload.modules).toContain("./app.py"); + expect(upload.modules).toContain("lib.cjs"); + expect(upload.modules).toContain("data.bin"); + expect(upload.modules).toContain("text.txt"); + expect(upload.modules).toContain("helper.wasm"); + expect(upload.modules).toContain("config.json"); + expect(upload.modules).toContain("app.py"); expect(std.out).toContain("Deployed"); }); }); diff --git a/packages/cli/src/__tests__/lib/deploy-input.test.ts b/packages/cli/src/__tests__/lib/deploy-input.test.ts index 7902434314..74fca37236 100644 --- a/packages/cli/src/__tests__/lib/deploy-input.test.ts +++ b/packages/cli/src/__tests__/lib/deploy-input.test.ts @@ -47,7 +47,7 @@ describe("transforms Build Output Specification into deploy input", () => { expect(result.bundleType).toBe("esm"); expect(result.content).toBe("export default {}"); expect(result.modules).toHaveLength(1); - expect(result.modules[0]?.name).toBe("./helper.wasm"); + expect(result.modules[0]?.name).toBe("helper.wasm"); expect(result.modules[0]?.type).toBe("compiled-wasm"); expect(Object.keys(result.dependencies)).toHaveLength(2); }); @@ -83,9 +83,9 @@ describe("transforms Build Output Specification into deploy input", () => { expect(builtConfig.manifest?.type).toBe("complete"); expect(result.modules).toEqual( expect.arrayContaining([ - expect.objectContaining({ name: "./chunk.mjs", type: "esm" }), + expect.objectContaining({ name: "chunk.mjs", type: "esm" }), expect.objectContaining({ - name: "./helper.wasm", + name: "helper.wasm", type: "compiled-wasm", }), ]) @@ -266,11 +266,55 @@ describe("transforms Build Output Specification into deploy input", () => { const typeMap = Object.fromEntries( result.modules.map((m) => [m.name, m.type]) ); - expect(typeMap["./lib.cjs"]).toBe("commonjs"); - expect(typeMap["./app.py"]).toBe("python"); - expect(typeMap["./dep.txt"]).toBe("text"); - expect(typeMap["./blob.bin"]).toBe("buffer"); - expect(typeMap["./config.json"]).toBe("text"); + expect(typeMap["lib.cjs"]).toBe("commonjs"); + expect(typeMap["app.py"]).toBe("python"); + expect(typeMap["dep.txt"]).toBe("text"); + expect(typeMap["blob.bin"]).toBe("buffer"); + expect(typeMap["config.json"]).toBe("text"); + }); + + it("keeps Python vendored module paths exactly as in the manifest", async () => { + // Python Workers import vendored packages from `python_modules/` and use + // module names as file paths, so a `./` prefix makes `import workers` fail. + await seed({ + ".cloudflare/output/v0/config.json": buildOutputRootConfig(), + ".cloudflare/output/v0/workers/default/worker.config.json": workerConfig({ + name: "my-worker", + compatibilityFlags: ["python_workers"], + mainModule: "entry.py", + modules: { + "entry.py": { type: "python" }, + "helpers.py": { type: "python" }, + "python_modules/workers/__init__.py": { type: "data" }, + "python_modules/_workers_sdk_patch_loader.pth": { type: "data" }, + "python_modules/workers/workflows.js": { type: "esm" }, + }, + }), + ".cloudflare/output/v0/workers/default/bundle/entry.py": + "from workers import WorkerEntrypoint", + ".cloudflare/output/v0/workers/default/bundle/helpers.py": "pass", + ".cloudflare/output/v0/workers/default/bundle/python_modules/workers/__init__.py": + "", + ".cloudflare/output/v0/workers/default/bundle/python_modules/_workers_sdk_patch_loader.pth": + "", + ".cloudflare/output/v0/workers/default/bundle/python_modules/workers/workflows.js": + "export {}", + }); + + const { + rootConfig, + workers: { default: worker }, + } = await readBuildOutput(process.cwd()); + const { builtConfig } = parseWorkerConfig(worker, rootConfig); + const result = assembleBuildResult(worker, builtConfig); + + expect(result.bundleType).toBe("python"); + expect(result.modules.map((m) => m.name).sort()).toEqual([ + "helpers.py", + "python_modules/_workers_sdk_patch_loader.pth", + "python_modules/workers/__init__.py", + "python_modules/workers/workflows.js", + ]); }); it("throws when run_worker_first is set without a user Worker", async () => { diff --git a/packages/cli/src/lib/deploy-input.ts b/packages/cli/src/lib/deploy-input.ts index 6251236e7e..ff740173d5 100644 --- a/packages/cli/src/lib/deploy-input.ts +++ b/packages/cli/src/lib/deploy-input.ts @@ -274,8 +274,12 @@ export function assembleBuildResult( ? readFileSync(filePath) : readFileSync(filePath, "utf8"); + // Use the manifest key unchanged. The runtime normalises `./` in + // JavaScript module names, but Python Workers use module names as file + // paths without normalising them, so a `./python_modules/...` package + // is not on `sys.path`. cfModules.push({ - name: `./${name}`, + name, filePath, content: moduleContent, type: OUTPUT_TO_CF_MODULE_TYPE[meta.type],