From c192a03ca49aec8b108104dfe999e54193cba335 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 6 Apr 2026 18:09:13 +0900 Subject: [PATCH 1/7] feat(workers-runtime-sdk): support embedding javascript files in sdk --- packages/cli/tests/test_in_workerd.py | 3 + .../runtime-sdk/scripts/compile_js_sdk.py | 89 +++++++++++++++++++ packages/runtime-sdk/src/workers/_workers.py | 9 +- packages/runtime-sdk/src/workers/sdk.mjs | 36 ++++++++ .../runtime-sdk/tests/test_js_sdk_compile.py | 28 ++++++ packages/runtime-sdk/ts/sdk.ts | 41 +++++++++ packages/runtime-sdk/uv.lock | 2 +- 7 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 packages/runtime-sdk/scripts/compile_js_sdk.py create mode 100644 packages/runtime-sdk/src/workers/sdk.mjs create mode 100644 packages/runtime-sdk/tests/test_js_sdk_compile.py create mode 100644 packages/runtime-sdk/ts/sdk.ts diff --git a/packages/cli/tests/test_in_workerd.py b/packages/cli/tests/test_in_workerd.py index 98bf92d..4a23be3 100644 --- a/packages/cli/tests/test_in_workerd.py +++ b/packages/cli/tests/test_in_workerd.py @@ -38,8 +38,11 @@ def embed(dir: Path, root: Path, level: int = 0): modules.append( f'(name = "{module_path}", pythonModule = embed "{embed_path}")' ) + elif path.suffix == ".mjs": + modules.append(f'(name = "{module_path}", esModule = embed "{embed_path}")') else: modules.append(f'(name = "{module_path}", data = embed "{embed_path}")') + return modules diff --git a/packages/runtime-sdk/scripts/compile_js_sdk.py b/packages/runtime-sdk/scripts/compile_js_sdk.py new file mode 100644 index 0000000..a76d027 --- /dev/null +++ b/packages/runtime-sdk/scripts/compile_js_sdk.py @@ -0,0 +1,89 @@ +""" +Compile ts/sdk.ts -> src/workers/sdk.mjs using esbuild. + +Usage: + python scripts/compile_js_sdk.py # compile and write + python scripts/compile_js_sdk.py --check # verify sdk.mjs is up to date +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +RUNTIME_SDK_DIR = Path(__file__).resolve().parent.parent +TS_SOURCE = RUNTIME_SDK_DIR / "ts" / "sdk.ts" +MJS_OUTPUT = RUNTIME_SDK_DIR / "src" / "workers" / "sdk.mjs" + +HEADER = """\ +// AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY +// Source: ts/sdk.ts +// Regenerate: python scripts/compile_js_sdk.py +""" + + +def compile_ts() -> str: + """Compile ts/sdk.ts to JavaScript and return the output string (with header).""" + npx = shutil.which("npx") + if npx is None: + print( + "error: npx not found. Install Node.js to compile TypeScript.", + file=sys.stderr, + ) + sys.exit(1) + + result = subprocess.run( + [ + npx, + "--yes", + "esbuild", + str(TS_SOURCE), + "--format=esm", + "--log-level=error", + ], + capture_output=True, + text=True, + cwd=str(RUNTIME_SDK_DIR), + check=False, + ) + + if result.returncode != 0: + print(f"esbuild failed:\n{result.stderr}", file=sys.stderr) + sys.exit(result.returncode) + + return HEADER + result.stdout + + +def main() -> None: + compiled = compile_ts() + + if "--check" in sys.argv: + if not MJS_OUTPUT.exists(): + print( + f"error: {MJS_OUTPUT.relative_to(RUNTIME_SDK_DIR)} does not exist.", + file=sys.stderr, + ) + print("Run: python scripts/compile_js_sdk.py", file=sys.stderr) + sys.exit(1) + + current = MJS_OUTPUT.read_text() + if current != compiled: + print( + f"error: {MJS_OUTPUT.relative_to(RUNTIME_SDK_DIR)} is out of date.", + file=sys.stderr, + ) + print("Run: python scripts/compile_js_sdk.py", file=sys.stderr) + sys.exit(1) + + print(f"{MJS_OUTPUT.relative_to(RUNTIME_SDK_DIR)} is up to date.") + else: + MJS_OUTPUT.write_text(compiled) + print( + f"Compiled {TS_SOURCE.relative_to(RUNTIME_SDK_DIR)} -> {MJS_OUTPUT.relative_to(RUNTIME_SDK_DIR)}" + ) + + +if __name__ == "__main__": + main() diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index ca60380..c1fc671 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -108,6 +108,13 @@ def import_from_javascript(module_name: str) -> Any: raise +@functools.cache +def get_js_sdk(): + # IMPORTANT: The module name here must match how wrangler registers the module + # during the vendoring. Changing this value requires changes in workers-sdk. + return import_from_javascript("python_modules/workers/sdk.mjs") + + @contextmanager def patch_env( d: dict[str, Any] | Sequence[tuple[str, Any]] | None = None, **kwds: dict[str, Any] @@ -1332,7 +1339,7 @@ def _wrap_subclass(cls): def wrapped_init(self, *args, **kwargs): if len(args) > 0: - _pyodide_entrypoint_helper.patchWaitUntil(args[0]) + get_js_sdk().patchWaitUntil(args[0]) if len(args) > 1: args = list(args) args[1] = _EnvWrapper(args[1]) diff --git a/packages/runtime-sdk/src/workers/sdk.mjs b/packages/runtime-sdk/src/workers/sdk.mjs new file mode 100644 index 0000000..57c8361 --- /dev/null +++ b/packages/runtime-sdk/src/workers/sdk.mjs @@ -0,0 +1,36 @@ +// AUTO-GENERATED FILE - DO NOT EDIT DIRECTLY +// Source: ts/sdk.ts +// Regenerate: python scripts/compile_js_sdk.py +const waitUntilPatched = /* @__PURE__ */ new WeakSet(); +function patchWaitUntil(ctx) { + let tag; + try { + tag = Object.prototype.toString.call(ctx); + } catch (_e) { + } + if (tag !== "[object ExecutionContext]") { + return; + } + if (waitUntilPatched.has(ctx)) { + return; + } + const origWaitUntil = ctx.waitUntil.bind(ctx); + function waitUntil(p) { + origWaitUntil( + (async function() { + if ("copy" in p) { + p = p.copy(); + } + await p; + if ("destroy" in p) { + p.destroy(); + } + })() + ); + } + ctx.waitUntil = waitUntil; + waitUntilPatched.add(ctx); +} +export { + patchWaitUntil +}; diff --git a/packages/runtime-sdk/tests/test_js_sdk_compile.py b/packages/runtime-sdk/tests/test_js_sdk_compile.py new file mode 100644 index 0000000..95704f8 --- /dev/null +++ b/packages/runtime-sdk/tests/test_js_sdk_compile.py @@ -0,0 +1,28 @@ +"""Verify that sdk.mjs is up to date with the TypeScript source.""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +RUNTIME_SDK_DIR = Path(__file__).resolve().parent.parent +COMPILE_SCRIPT = RUNTIME_SDK_DIR / "scripts" / "compile_js_sdk.py" + + +def test_sdk_mjs_up_to_date() -> None: + """sdk.mjs must match the output of compiling ts/sdk.ts. + + If this test fails, run: + python scripts/compile_js_sdk.py + """ + result = subprocess.run( + [sys.executable, str(COMPILE_SCRIPT), "--check"], + capture_output=True, + text=True, + cwd=str(RUNTIME_SDK_DIR), + check=False, + ) + assert result.returncode == 0, ( + f"sdk.mjs is out of date. Run: python scripts/compile_js_sdk.py\n{result.stderr}" + ) diff --git a/packages/runtime-sdk/ts/sdk.ts b/packages/runtime-sdk/ts/sdk.ts new file mode 100644 index 0000000..b563dbe --- /dev/null +++ b/packages/runtime-sdk/ts/sdk.ts @@ -0,0 +1,41 @@ +// Javascript helper functions for workers-runtime-sdk +// This file is compiled to src/workers/sdk.mjs via scripts/compile_js_sdk.py + +// Pyodide proxy future — supports copy/destroy for proxy lifecycle management +interface PyFuture { + copy(): Promise; + destroy(): void; +} + +const waitUntilPatched = new WeakSet(); + +export function patchWaitUntil(ctx: { + waitUntil: (p: Promise | PyFuture) => void; +}): void { + let tag; + try { + tag = Object.prototype.toString.call(ctx); + } catch (_e) {} + if (tag !== '[object ExecutionContext]') { + return; + } + if (waitUntilPatched.has(ctx)) { + return; + } + const origWaitUntil: (p: Promise) => void = ctx.waitUntil.bind(ctx); + function waitUntil(p: Promise | PyFuture): void { + origWaitUntil( + (async function (): Promise { + if ('copy' in p) { + p = p.copy(); + } + await p; + if ('destroy' in p) { + p.destroy(); + } + })() + ); + } + ctx.waitUntil = waitUntil; + waitUntilPatched.add(ctx); +} diff --git a/packages/runtime-sdk/uv.lock b/packages/runtime-sdk/uv.lock index 679ee65..8a8f353 100644 --- a/packages/runtime-sdk/uv.lock +++ b/packages/runtime-sdk/uv.lock @@ -210,7 +210,7 @@ wheels = [ [[package]] name = "workers-runtime-sdk" -version = "1.1.0" +version = "1.1.1" source = { editable = "." } [package.dev-dependencies] From 65b71763a3ffec8063f460c70e6fa83ac53daab1 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 6 Apr 2026 18:15:24 +0900 Subject: [PATCH 2/7] chore: revert uv.lock change --- packages/runtime-sdk/uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-sdk/uv.lock b/packages/runtime-sdk/uv.lock index 8a8f353..679ee65 100644 --- a/packages/runtime-sdk/uv.lock +++ b/packages/runtime-sdk/uv.lock @@ -210,7 +210,7 @@ wheels = [ [[package]] name = "workers-runtime-sdk" -version = "1.1.1" +version = "1.1.0" source = { editable = "." } [package.dev-dependencies] From 3ed50707cc69562c30c2c4e92a5290515d330477 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 13 Apr 2026 22:35:48 +0900 Subject: [PATCH 3/7] chore: Update packages/runtime-sdk/ts/sdk.ts Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com> --- packages/runtime-sdk/ts/sdk.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime-sdk/ts/sdk.ts b/packages/runtime-sdk/ts/sdk.ts index b563dbe..6d9ca2e 100644 --- a/packages/runtime-sdk/ts/sdk.ts +++ b/packages/runtime-sdk/ts/sdk.ts @@ -2,10 +2,10 @@ // This file is compiled to src/workers/sdk.mjs via scripts/compile_js_sdk.py // Pyodide proxy future — supports copy/destroy for proxy lifecycle management -interface PyFuture { - copy(): Promise; +type PyFuture = Promise & { + copy(): PyFuture; destroy(): void; -} +}; const waitUntilPatched = new WeakSet(); From 91a578af4e4b2d5bc0a512cd21d09b8c5f3098a7 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 13 Apr 2026 22:37:44 +0900 Subject: [PATCH 4/7] chore: test sdk directory --- .github/workflows/tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7e1e984..252d5e6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,11 +30,16 @@ jobs: run: | uv pip install --system -e packages/runtime-sdk -e packages/cli pytest - - name: Run tests + - name: Run cli tests working-directory: packages/cli run: | pytest tests + - name: Run runtime-sdk tests + working-directory: packages/runtime-sdk + run: | + pytest tests + - name: Verify that pywrangler can be run globally run: | pywrangler --help From fdda13f974621dd72c3fbea925c5081bc2970378 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 13 Apr 2026 22:37:52 +0900 Subject: [PATCH 5/7] chore: pin esbuild version --- packages/runtime-sdk/scripts/compile_js_sdk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-sdk/scripts/compile_js_sdk.py b/packages/runtime-sdk/scripts/compile_js_sdk.py index a76d027..accc008 100644 --- a/packages/runtime-sdk/scripts/compile_js_sdk.py +++ b/packages/runtime-sdk/scripts/compile_js_sdk.py @@ -38,7 +38,7 @@ def compile_ts() -> str: [ npx, "--yes", - "esbuild", + "esbuild@0.28.0", str(TS_SOURCE), "--format=esm", "--log-level=error", From be1d6903223e912ba7acc2dd4897de45a1fb4fd2 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Mon, 13 Apr 2026 22:41:12 +0900 Subject: [PATCH 6/7] chore: tidy up comment --- packages/runtime-sdk/src/workers/_workers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index c1fc671..75700b5 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -111,7 +111,7 @@ def import_from_javascript(module_name: str) -> Any: @functools.cache def get_js_sdk(): # IMPORTANT: The module name here must match how wrangler registers the module - # during the vendoring. Changing this value requires changes in workers-sdk. + # while vendoring the python_modules. return import_from_javascript("python_modules/workers/sdk.mjs") @@ -1339,7 +1339,8 @@ def _wrap_subclass(cls): def wrapped_init(self, *args, **kwargs): if len(args) > 0: - get_js_sdk().patchWaitUntil(args[0]) + js_sdk = get_js_sdk() + js_sdk.patchWaitUntil(args[0]) if len(args) > 1: args = list(args) args[1] = _EnvWrapper(args[1]) From a4a7d8b531352adac6de2d93c15aaf081f9d0cab Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Fri, 17 Apr 2026 18:56:08 +0900 Subject: [PATCH 7/7] chore: tidy up --- packages/runtime-sdk/scripts/compile_js_sdk.py | 2 -- packages/runtime-sdk/src/workers/_workers.py | 6 ++++-- packages/runtime-sdk/tests/test_js_sdk_compile.py | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/runtime-sdk/scripts/compile_js_sdk.py b/packages/runtime-sdk/scripts/compile_js_sdk.py index accc008..b606e38 100644 --- a/packages/runtime-sdk/scripts/compile_js_sdk.py +++ b/packages/runtime-sdk/scripts/compile_js_sdk.py @@ -6,8 +6,6 @@ python scripts/compile_js_sdk.py --check # verify sdk.mjs is up to date """ -from __future__ import annotations - import shutil import subprocess import sys diff --git a/packages/runtime-sdk/src/workers/_workers.py b/packages/runtime-sdk/src/workers/_workers.py index 75700b5..bfd676e 100644 --- a/packages/runtime-sdk/src/workers/_workers.py +++ b/packages/runtime-sdk/src/workers/_workers.py @@ -110,8 +110,10 @@ def import_from_javascript(module_name: str) -> Any: @functools.cache def get_js_sdk(): - # IMPORTANT: The module name here must match how wrangler registers the module - # while vendoring the python_modules. + # IMPORTANT: + # The module name here must match how wrangler registers the JS modules + # while vendoring the python_modules directory. + # See: https://github.com/cloudflare/workers-sdk/pull/13311 return import_from_javascript("python_modules/workers/sdk.mjs") diff --git a/packages/runtime-sdk/tests/test_js_sdk_compile.py b/packages/runtime-sdk/tests/test_js_sdk_compile.py index 95704f8..4e8d675 100644 --- a/packages/runtime-sdk/tests/test_js_sdk_compile.py +++ b/packages/runtime-sdk/tests/test_js_sdk_compile.py @@ -1,7 +1,5 @@ """Verify that sdk.mjs is up to date with the TypeScript source.""" -from __future__ import annotations - import subprocess import sys from pathlib import Path