presets(cloudflare): support durable objects, workflows and bindings in local dev#4341
presets(cloudflare): support durable objects, workflows and bindings in local dev#4341zsilbi wants to merge 4 commits into
Conversation
|
@zsilbi is attempting to deploy a commit to the Nitro Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR adds Cloudflare Durable Objects and Workflows support to Nitro by introducing a dev-worker composition system that bundles exports from ChangesCloudflare Durable Objects Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
commit: |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/cloudflare-durable/routes/counter.ts`:
- Around line 4-8: The code assumes event.req.runtime?.cloudflare?.env exists
and directly casts it, which will throw a TypeError if missing; update the
handler (defineHandler) to validate that event.req.runtime?.cloudflare?.env is
defined and that env.COUNTER exists before calling
env.COUNTER.getByName("global"), and throw a clear, descriptive error (e.g.,
"Missing Cloudflare Durable Object binding: COUNTER") so callers get an
actionable message instead of a runtime TypeError; reference the env variable
and the COUNTER DurableObjectNamespace/CounterDO binding when adding the checks.
In `@src/presets/cloudflare/dev.ts`:
- Around line 149-153: IPC can call loadDevWorker() before
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ is set, so guard and lazily initialize
that binding instead of dereferencing it; create a small helper (e.g.,
ensureUnsafeEval or createUnsafeEvalBinder) that if
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ is undefined will initialize and assign a
safe fallback object exposing newAsyncFunction (built via the Function
constructor to return import(id) so it behaves like the real binder), then use
that helper when computing devWorkerPromise and in the other block (the similar
166-173 code) so loadDevWorker() never calls undefined.newAsyncFunction;
reference globalThis.__ENV_RUNNER_UNSAFE_EVAL__, devWorkerPromise,
loadDevWorker, newAsyncFunction, devWorkerId, and the ipc.onOpen / ipc.onMessage
pre-load paths when locating where to add the helper and replace direct
dereferences.
- Around line 152-154: The memoized devWorkerPromise (devWorkerPromise) is set
with ??= which leaves a permanently rejected promise on transient import
failures; update the promise chain returned by
newAsyncFunction(...)(devWorkerId) to attach a .catch handler that clears
devWorkerPromise (set it to undefined/null) before rethrowing the error so
subsequent attempts will retry, while keeping the successful .then branch that
sets devWorker; reference devWorkerPromise, devWorker, devWorkerId and the
newAsyncFunction(...).then(...) chain when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c8e33f58-1f23-4169-8454-e9ebef5545cd
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (23)
docs/2.deploy/20.providers/cloudflare.mdexamples/cloudflare-durable/README.mdexamples/cloudflare-durable/exports.cloudflare.tsexamples/cloudflare-durable/index.htmlexamples/cloudflare-durable/nitro.config.tsexamples/cloudflare-durable/package.jsonexamples/cloudflare-durable/routes/counter.tsexamples/cloudflare-durable/server/durable/counter.tsexamples/cloudflare-durable/tsconfig.jsonexamples/cloudflare-durable/vite.config.tsexamples/cloudflare-durable/wrangler.jsoncsrc/build/vite/env.tssrc/presets/cloudflare/dev.tssrc/presets/cloudflare/entry-exports.tssrc/presets/cloudflare/utils.tssrc/runtime/internal/vite/dev-worker.mjstest/examples.test.tstest/vite/cloudflare-do-fixture/durable/counter.tstest/vite/cloudflare-do-fixture/durable/entrypoint.tstest/vite/cloudflare-do-fixture/exports.cloudflare.tstest/vite/cloudflare-do-fixture/routes/counter.tstest/vite/cloudflare-do-fixture/vite.config.tstest/vite/cloudflare-do.test.ts
| export default defineHandler(async (event) => { | ||
| const env = event.req.runtime?.cloudflare?.env as { | ||
| COUNTER: DurableObjectNamespace<CounterDO>; | ||
| }; | ||
| const counter = env.COUNTER.getByName("global"); |
There was a problem hiding this comment.
Add validation for missing Cloudflare bindings.
If event.req.runtime?.cloudflare?.env is undefined, the type assertion on line 5 combined with property access on line 8 will throw a TypeError rather than a clear, actionable error. As per coding guidelines, prefer explicit errors over silent failures.
🛡️ Proposed fix to add explicit validation
export default defineHandler(async (event) => {
+ if (!event.req.runtime?.cloudflare?.env) {
+ throw new Error("Cloudflare runtime environment not available");
+ }
const env = event.req.runtime?.cloudflare?.env as {
COUNTER: DurableObjectNamespace<CounterDO>;
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/cloudflare-durable/routes/counter.ts` around lines 4 - 8, The code
assumes event.req.runtime?.cloudflare?.env exists and directly casts it, which
will throw a TypeError if missing; update the handler (defineHandler) to
validate that event.req.runtime?.cloudflare?.env is defined and that env.COUNTER
exists before calling env.COUNTER.getByName("global"), and throw a clear,
descriptive error (e.g., "Missing Cloudflare Durable Object binding: COUNTER")
so callers get an actionable message instead of a runtime TypeError; reference
the env variable and the COUNTER DurableObjectNamespace/CounterDO binding when
adding the checks.
Source: Coding guidelines
| if (env && !globalThis.__ENV_RUNNER_UNSAFE_EVAL__) { | ||
| globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__; | ||
| } | ||
| return (devWorkerPromise ??= globalThis.__ENV_RUNNER_UNSAFE_EVAL__ | ||
| .newAsyncFunction("return import(id)", "loadDevWorker", "id")(devWorkerId) |
There was a problem hiding this comment.
IPC can hit loadDevWorker() before the unsafe-eval binding is initialized.
fetch() seeds globalThis.__ENV_RUNNER_UNSAFE_EVAL__ from env, but ipc.onOpen() and the pre-load ipc.onMessage() path call loadDevWorker() with no env. On a fresh worker that can dereference undefined.newAsyncFunction(...) before the first request, which breaks the dev-worker/HMR bootstrap.
Also applies to: 166-173
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/presets/cloudflare/dev.ts` around lines 149 - 153, IPC can call
loadDevWorker() before globalThis.__ENV_RUNNER_UNSAFE_EVAL__ is set, so guard
and lazily initialize that binding instead of dereferencing it; create a small
helper (e.g., ensureUnsafeEval or createUnsafeEvalBinder) that if
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ is undefined will initialize and assign a
safe fallback object exposing newAsyncFunction (built via the Function
constructor to return import(id) so it behaves like the real binder), then use
that helper when computing devWorkerPromise and in the other block (the similar
166-173 code) so loadDevWorker() never calls undefined.newAsyncFunction;
reference globalThis.__ENV_RUNNER_UNSAFE_EVAL__, devWorkerPromise,
loadDevWorker, newAsyncFunction, devWorkerId, and the ipc.onOpen / ipc.onMessage
pre-load paths when locating where to add the helper and replace direct
dereferences.
| return (devWorkerPromise ??= globalThis.__ENV_RUNNER_UNSAFE_EVAL__ | ||
| .newAsyncFunction("return import(id)", "loadDevWorker", "id")(devWorkerId) | ||
| .then((mod) => (devWorker = mod))); |
There was a problem hiding this comment.
Reset the memoized import promise on failure.
With ??=, one transient transform/import failure leaves devWorkerPromise permanently rejected, so every later request keeps failing until the whole runner restarts.
Suggested fix
function loadDevWorker(env) {
if (env && !globalThis.__ENV_RUNNER_UNSAFE_EVAL__) {
globalThis.__ENV_RUNNER_UNSAFE_EVAL__ = env.__ENV_RUNNER_UNSAFE_EVAL__;
}
return (devWorkerPromise ??= globalThis.__ENV_RUNNER_UNSAFE_EVAL__
.newAsyncFunction("return import(id)", "loadDevWorker", "id")(devWorkerId)
- .then((mod) => (devWorker = mod)));
+ .then((mod) => (devWorker = mod))
+ .catch((error) => {
+ devWorkerPromise = undefined;
+ throw error;
+ }));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return (devWorkerPromise ??= globalThis.__ENV_RUNNER_UNSAFE_EVAL__ | |
| .newAsyncFunction("return import(id)", "loadDevWorker", "id")(devWorkerId) | |
| .then((mod) => (devWorker = mod))); | |
| return (devWorkerPromise ??= globalThis.__ENV_RUNNER_UNSAFE_EVAL__ | |
| .newAsyncFunction("return import(id)", "loadDevWorker", "id")(devWorkerId) | |
| .then((mod) => (devWorker = mod)) | |
| .catch((error) => { | |
| devWorkerPromise = undefined; | |
| throw error; | |
| })); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/presets/cloudflare/dev.ts` around lines 152 - 154, The memoized
devWorkerPromise (devWorkerPromise) is set with ??= which leaves a permanently
rejected promise on transient import failures; update the promise chain returned
by newAsyncFunction(...)(devWorkerId) to attach a .catch handler that clears
devWorkerPromise (set it to undefined/null) before rethrowing the error so
subsequent attempts will retry, while keeping the successful .then branch that
sets devWorker; reference devWorkerPromise, devWorker, devWorkerId and the
newAsyncFunction(...).then(...) chain when making the change.
Follow-up to #4338 — makes durable object and workflows usable in
dev(miniflare runner).🔗 Linked issue
#4339
❓ Type of change
📚 Description
Compose a dev worker entry that statically re-exports binding-referenced classes from
exports.cloudflare.ts(bundled with rolldown) and pass them to env-runner viaexports— workerd requires them as static exports, while the app keeps loading through the Vite module runner (HMR intact, DO class changes need a restart - documented).Forward
env/ctxto the app in the dev worker, same as the prod module handler (globalThis.__env__+req.runtime.cloudflare).📝 Checklist