From 5b5cbd3e98e5713ecf5ee0afa975a1f2ee38b2cc Mon Sep 17 00:00:00 2001 From: Maryna Iholnykova <57571831+Refaerds@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:30:50 +0200 Subject: [PATCH 1/9] [wrangler] update browser binding generated type (#14135) --- .changeset/browser-run-type.md | 7 +++++++ packages/wrangler/src/__tests__/type-generation.test.ts | 6 +++--- packages/wrangler/src/type-generation/index.ts | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .changeset/browser-run-type.md diff --git a/.changeset/browser-run-type.md b/.changeset/browser-run-type.md new file mode 100644 index 0000000000..16fd8e122b --- /dev/null +++ b/.changeset/browser-run-type.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Update the generated type for browser bindings to `BrowserRun` + +When running `wrangler types`, browser bindings were previously typed as the generic `Fetcher`. They now generate the more specific and accurate `BrowserRun` type. diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 0095321b3f..87207ae930 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -797,7 +797,7 @@ describe("generate types - CLI", () => { AI_SEARCH_BINDING: AiSearchInstance; AGENT_MEMORY_BINDING: AgentMemoryNamespace; LOGFWDR_SCHEMA: any; - BROWSER_BINDING: Fetcher; + BROWSER_BINDING: BrowserRun; AI_BINDING: Ai; IMAGES_BINDING: ImagesBinding; STREAM_BINDING: StreamBinding; @@ -917,7 +917,7 @@ describe("generate types - CLI", () => { AI_SEARCH_BINDING: AiSearchInstance; AGENT_MEMORY_BINDING: AgentMemoryNamespace; LOGFWDR_SCHEMA: any; - BROWSER_BINDING: Fetcher; + BROWSER_BINDING: BrowserRun; AI_BINDING: Ai; IMAGES_BINDING: ImagesBinding; STREAM_BINDING: StreamBinding; @@ -1100,7 +1100,7 @@ describe("generate types - CLI", () => { AI_SEARCH_BINDING: AiSearchInstance; AGENT_MEMORY_BINDING: AgentMemoryNamespace; LOGFWDR_SCHEMA: any; - BROWSER_BINDING: Fetcher; + BROWSER_BINDING: BrowserRun; AI_BINDING: Ai; IMAGES_BINDING: ImagesBinding; STREAM_BINDING: StreamBinding; diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index abd6803388..426cf72629 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -2388,7 +2388,7 @@ function collectCoreBindings( fieldName: "binding", }); } else { - addBinding(env.browser.binding, "Fetcher", "browser", envName); + addBinding(env.browser.binding, "BrowserRun", "browser", envName); } } @@ -3373,7 +3373,7 @@ function collectCoreBindingsPerEnvironment( bindings.push({ bindingCategory: "browser", name: env.browser.binding, - type: "Fetcher", + type: "BrowserRun", }); } } From 8388c355961d69ce9b099699be92204ececcbbd1 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 1 Jun 2026 11:35:39 +0100 Subject: [PATCH 2/9] tools: use esbuild to extract bare imports in dist validator (#14137) --- package.json | 2 +- .../validate-package-dependencies.test.ts | 117 +++++++++++------- .../validate-package-dependencies.ts | 74 ++++++----- 3 files changed, 110 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 422a9b2d4b..521a540eb4 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "check:fixtures": "node -r esbuild-register tools/deployments/validate-fixtures.ts", "check:format": "oxfmt --check", "check:lint": "oxlint --deny-warnings --type-aware", - "check:package-deps": "node -r esbuild-register tools/deployments/validate-package-dependencies.ts", + "check:package-deps": "pnpm build && node -r esbuild-register tools/deployments/validate-package-dependencies.ts", "check:private-packages": "node -r esbuild-register tools/deployments/validate-private-packages.ts", "check:type": "dotenv -- turbo check:type type:tests", "check:workflows": "node -r esbuild-register tools/github-workflow-helpers/validate-action-pinning.ts", diff --git a/tools/deployments/__tests__/validate-package-dependencies.test.ts b/tools/deployments/__tests__/validate-package-dependencies.test.ts index 96e0537a71..c9291068da 100644 --- a/tools/deployments/__tests__/validate-package-dependencies.test.ts +++ b/tools/deployments/__tests__/validate-package-dependencies.test.ts @@ -379,18 +379,6 @@ describe("isBareSpecifier()", () => { expect(isBareSpecifier("__BUILD_CONFIG")).toBe(false); }); - it("should reject template-literal expressions in specifiers", ({ - expect, - }) => { - expect(isBareSpecifier("${moduleName}")).toBe(false); - expect(isBareSpecifier("foo/${bar}")).toBe(false); - }); - - it("should reject specifiers with wildcards", ({ expect }) => { - expect(isBareSpecifier("*")).toBe(false); - expect(isBareSpecifier("foo/*")).toBe(false); - }); - it("should reject specifiers that are not package-name shaped", ({ expect, }) => { @@ -401,105 +389,146 @@ describe("isBareSpecifier()", () => { }); describe("extractBareImports()", () => { - it("should extract named imports", ({ expect }) => { - const imports = extractBareImports(`import { x } from "lodash";`); + it("should extract named imports", async ({ expect }) => { + const imports = await extractBareImports(`import { x } from "lodash";`); expect([...imports]).toEqual(["lodash"]); }); - it("should extract default imports", ({ expect }) => { - const imports = extractBareImports(`import x from "lodash";`); + it("should extract default imports", async ({ expect }) => { + const imports = await extractBareImports(`import x from "lodash";`); expect([...imports]).toEqual(["lodash"]); }); - it("should extract namespace imports", ({ expect }) => { - const imports = extractBareImports(`import * as x from "lodash";`); + it("should extract namespace imports", async ({ expect }) => { + const imports = await extractBareImports(`import * as x from "lodash";`); expect([...imports]).toEqual(["lodash"]); }); - it("should extract side-effect imports", ({ expect }) => { - const imports = extractBareImports(`import "lodash";`); + it("should extract side-effect imports", async ({ expect }) => { + const imports = await extractBareImports(`import "lodash";`); expect([...imports]).toEqual(["lodash"]); }); - it("should extract re-exports", ({ expect }) => { - const imports = extractBareImports(`export { x } from "lodash";`); + it("should extract re-exports", async ({ expect }) => { + const imports = await extractBareImports(`export { x } from "lodash";`); expect([...imports]).toEqual(["lodash"]); }); - it("should extract require() calls", ({ expect }) => { - const imports = extractBareImports( + it("should extract require() calls", async ({ expect }) => { + const imports = await extractBareImports( `const x = require("lodash"); require("foo");` ); expect([...imports].sort()).toEqual(["foo", "lodash"]); }); - it("should extract dynamic import() calls with string literals", ({ + it("should extract dynamic import() calls with string literals", async ({ expect, }) => { - const imports = extractBareImports(`const x = await import("lodash");`); + const imports = await extractBareImports( + `const x = await import("lodash");` + ); expect([...imports]).toEqual(["lodash"]); }); - it("should strip subpath imports down to package name", ({ expect }) => { - const imports = extractBareImports( + it("should strip subpath imports down to package name", async ({ + expect, + }) => { + const imports = await extractBareImports( `import x from "semver/functions/satisfies.js";` ); expect([...imports]).toEqual(["semver"]); }); - it("should skip relative imports", ({ expect }) => { - const imports = extractBareImports( + it("should skip relative imports", async ({ expect }) => { + const imports = await extractBareImports( `import x from "./foo"; import y from "../bar";` ); expect([...imports]).toEqual([]); }); - it("should skip Node built-in imports", ({ expect }) => { - const imports = extractBareImports( + it("should skip Node built-in imports", async ({ expect }) => { + const imports = await extractBareImports( `import fs from "node:fs"; const path = require("node:path");` ); expect([...imports]).toEqual([]); }); - it("should skip Cloudflare/workerd built-in imports", ({ expect }) => { - const imports = extractBareImports( + it("should skip Cloudflare/workerd built-in imports", async ({ expect }) => { + const imports = await extractBareImports( `import { env } from "cloudflare:workers"; import x from "workerd:unsafe";` ); expect([...imports]).toEqual([]); }); - it("should skip imports inside line comments", ({ expect }) => { - const imports = extractBareImports( + it("should skip imports inside line comments", async ({ expect }) => { + const imports = await extractBareImports( `// import x from "lodash";\nimport y from "react";` ); expect([...imports]).toEqual(["react"]); }); - it("should skip imports inside block comments", ({ expect }) => { - const imports = extractBareImports( + it("should skip imports inside block comments", async ({ expect }) => { + const imports = await extractBareImports( `/* import x from "lodash"; */\nimport y from "react";` ); expect([...imports]).toEqual(["react"]); }); - it("should not match `from` keywords that aren't part of import/export", ({ + it("should skip imports inside template literals (regression: createViteConfig)", async ({ + expect, + }) => { + // Regression test: wrangler's createViteConfig bundles the literal text + // of a vite.config.ts into a template literal. The regex-based scanner + // used to match `import { defineConfig } from "vite"` as a real import. + const imports = await extractBareImports( + [ + "function createViteConfig() {", + ' const content = `import { cloudflare } from "@cloudflare/vite-plugin";', + 'import { defineConfig } from "vite";', + "", + "export default defineConfig({", + "\tplugins: [cloudflare()],", + "});", + "`;", + " return content;", + "}", + 'import real from "react";', + ].join("\n") + ); + expect([...imports]).toEqual(["react"]); + }); + + it("should skip imports inside single- and double-quoted strings", async ({ + expect, + }) => { + const imports = await extractBareImports( + [ + 'const a = "import { x } from \\"lodash\\";";', + "const b = 'import { y } from \"react\";';", + 'import real from "commander";', + ].join("\n") + ); + expect([...imports]).toEqual(["commander"]); + }); + + it("should not match `from` keywords that aren't part of import/export", async ({ expect, }) => { - const imports = extractBareImports( + const imports = await extractBareImports( `function foo() { return "hello"; } const z = "from";` ); expect([...imports]).toEqual([]); }); - it("should deduplicate imports", ({ expect }) => { - const imports = extractBareImports( + it("should deduplicate imports", async ({ expect }) => { + const imports = await extractBareImports( `import { x } from "lodash";\nimport { y } from "lodash";` ); expect([...imports]).toEqual(["lodash"]); }); - it("should handle multiple imports in one file", ({ expect }) => { - const imports = extractBareImports(` + it("should handle multiple imports in one file", async ({ expect }) => { + const imports = await extractBareImports(` import { x } from "lodash"; import y from "react"; import "polyfill"; diff --git a/tools/deployments/validate-package-dependencies.ts b/tools/deployments/validate-package-dependencies.ts index bcd4ee318a..47f3b4b21f 100644 --- a/tools/deployments/validate-package-dependencies.ts +++ b/tools/deployments/validate-package-dependencies.ts @@ -24,6 +24,7 @@ import { existsSync, readFileSync } from "node:fs"; import { isBuiltin } from "node:module"; import { dirname, resolve } from "node:path"; +import * as esbuild from "esbuild"; import { glob } from "tinyglobby"; export interface PackageJSON { @@ -269,12 +270,6 @@ export function isBareSpecifier(spec: string): boolean { if (/^__[A-Z][A-Z0-9_]*$/.test(spec)) { return false; } - // Specifiers containing template-literal expressions are dynamic strings - // that appear inside source-code templates emitted by Wrangler, not real - // imports of the wrapping package. - if (spec.includes("${") || spec.includes("*")) { - return false; - } // Specifier must look like a valid npm package name: lowercase letters/digits, // hyphens, dots, underscores; optionally scoped (@scope/name). npm package // names cannot contain uppercase letters per the npm naming rules. @@ -300,41 +295,44 @@ export function isBareSpecifier(spec: string): boolean { * * Returns top-level package names (e.g. "vitest" for "vitest/runtime"). * - * This is a regex-based scanner, not a real parser — it can produce false - * positives when bundled output contains string literals that look like - * import statements (e.g. JavaScript parser libraries shipped inside the - * bundle). False positives are filtered downstream by `isBareSpecifier` - * (rejecting non-package-shaped strings) and the self-import guard in - * `validateDistImports`. + * Uses esbuild's parser (via a `bundle: true` build with everything marked + * external) so that specifiers found inside string literals, template + * literals, comments, etc. are correctly ignored. `bundle: true` is required + * to surface `require()` calls in the metafile — `bundle: false` only + * reports ESM `import` statements. The `externalize-all` plugin short- + * circuits resolution so esbuild never has to find the imports on disk. */ -export function extractBareImports(content: string): Set { +export async function extractBareImports( + content: string +): Promise> { const imports = new Set(); - // Strip line comments and block comments to avoid matching specs inside them. - // This is a deliberate approximation — sufficient for built/minified output. - const stripped = content - .replace(/\/\*[\s\S]*?\*\//g, "") - .replace(/\/\/[^\n]*/g, ""); - - const patterns: RegExp[] = [ - // import ... from "spec" / export ... from "spec" - // Anchored to a statement boundary (start of file, newline, `;`, or `}`) - // to avoid matching `from "x"` inside string literals. - /(?:^|[\n;}])\s*(?:import|export)\b[^"';\n]*?\bfrom\s*["']([^"'\n]+)["']/g, - // import "spec" (side-effect import) — also statement-anchored. - /(?:^|[\n;}])\s*import\s*["']([^"'\n]+)["']/g, - // require("spec") - /\brequire\s*\(\s*["']([^"'\n]+)["']\s*\)/g, - // import("spec") — only matches string literals - /\bimport\s*\(\s*["']([^"'\n]+)["']\s*\)/g, - ]; + const result = await esbuild.build({ + stdin: { contents: content, loader: "js" }, + metafile: true, + bundle: true, + write: false, + logLevel: "silent", + platform: "neutral", + plugins: [ + { + name: "externalize-all", + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => { + if (args.kind === "entry-point") { + return undefined; + } + return { path: args.path, external: true }; + }); + }, + }, + ], + }); - for (const re of patterns) { - let m: RegExpExecArray | null; - while ((m = re.exec(stripped)) !== null) { - const spec = m[1]; - if (isBareSpecifier(spec)) { - imports.add(getPackageNameFromSpecifier(spec)); + for (const input of Object.values(result.metafile.inputs)) { + for (const imp of input.imports) { + if (isBareSpecifier(imp.path)) { + imports.add(getPackageNameFromSpecifier(imp.path)); } } } @@ -441,7 +439,7 @@ export async function scanDistForExternalImports( continue; } const content = readFileSync(file, "utf-8"); - for (const imp of extractBareImports(content)) { + for (const imp of await extractBareImports(content)) { imports.add(imp); } } From a5b76906ec568eb6ad096dc166c5b6228040acb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matt=20=E2=80=98TK=E2=80=99=20Taylor?= Date: Mon, 1 Jun 2026 11:52:33 +0100 Subject: [PATCH 3/9] [C3] Migrate TanStack Start scaffolding to @tanstack/cli (#14096) --- .changeset/c3-tanstack-cli-migration.md | 7 +++++++ packages/create-cloudflare/src/frameworks/package.json | 2 +- packages/create-cloudflare/templates/tanstack-start/c3.ts | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/c3-tanstack-cli-migration.md diff --git a/.changeset/c3-tanstack-cli-migration.md b/.changeset/c3-tanstack-cli-migration.md new file mode 100644 index 0000000000..6cf2e6e280 --- /dev/null +++ b/.changeset/c3-tanstack-cli-migration.md @@ -0,0 +1,7 @@ +--- +"create-cloudflare": minor +--- + +Migrate TanStack Start scaffolding from `@tanstack/create-start` to `@tanstack/cli` + +TanStack has consolidated their project scaffolding into a unified CLI package (`@tanstack/cli`) with a `create` subcommand, replacing the previous `@tanstack/create-start` package. This updates C3 to use the new CLI while preserving the same Cloudflare deployment target and React framework options. diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index aa94b3bdb7..16eedd8eb1 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -2,7 +2,7 @@ "name": "frameworks_clis_info", "dependencies": { "@angular/create": "21.2.12", - "@tanstack/create-start": "0.59.32", + "@tanstack/cli": "0.68.0", "create-analog": "2.5.2", "create-astro": "5.0.6", "create-docusaurus": "3.10.1", diff --git a/packages/create-cloudflare/templates/tanstack-start/c3.ts b/packages/create-cloudflare/templates/tanstack-start/c3.ts index 324e267b55..fccdab27ba 100644 --- a/packages/create-cloudflare/templates/tanstack-start/c3.ts +++ b/packages/create-cloudflare/templates/tanstack-start/c3.ts @@ -8,6 +8,8 @@ const { npm } = detectPackageManager(); const generate = async (ctx: C3Context) => { await runFrameworkGenerator(ctx, [ + // @tanstack/cli uses `create` as a subcommand + "create", ctx.project.name, "--deployment", "cloudflare", @@ -24,7 +26,7 @@ const config: TemplateConfig = { configVersion: 1, id: "tanstack-start", platform: "workers", - frameworkCli: "@tanstack/create-start", + frameworkCli: "@tanstack/cli", displayName: "TanStack Start", generate, transformPackageJson: async () => ({ From 59e43e4e066f9d201fc6c1e3b31cb232853e83d7 Mon Sep 17 00:00:00 2001 From: MatinGathani <70268627+matingathani@users.noreply.github.com> Date: Mon, 1 Jun 2026 16:39:27 +0530 Subject: [PATCH 4/9] [wrangler] fix: remove trailing period after api-tokens URL in wrangler whoami output (#14133) --- .changeset/whoami-trailing-period.md | 10 ++++++++++ packages/wrangler/src/__tests__/deploy/core.test.ts | 2 +- packages/wrangler/src/user/whoami.ts | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/whoami-trailing-period.md diff --git a/.changeset/whoami-trailing-period.md b/.changeset/whoami-trailing-period.md new file mode 100644 index 0000000000..e2996ba1e7 --- /dev/null +++ b/.changeset/whoami-trailing-period.md @@ -0,0 +1,10 @@ +--- +"wrangler": patch +--- + +Fix `wrangler whoami` printing a trailing period after the api-tokens URL + +The message `To see token permissions visit https://...api-tokens.` ended with +a period that became part of the URL when clicked in terminals or GitHub Actions +output, causing a 404. The period is removed and a comma added before "visit" +so the sentence reads naturally without a trailing period on the URL. diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index 7b2c2871ca..cc53109208 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -529,7 +529,7 @@ describe("deploy", () => { ├─┼─┤ │ Account Three │ account-3 │ └─┴─┘ - 🔓 To see token permissions visit https://dash.cloudflare.com/profile/api-tokens.", + 🔓 To see token permissions, visit https://dash.cloudflare.com/profile/api-tokens", "warn": "", } `); diff --git a/packages/wrangler/src/user/whoami.ts b/packages/wrangler/src/user/whoami.ts index aae22b1529..94e39b1163 100644 --- a/packages/wrangler/src/user/whoami.ts +++ b/packages/wrangler/src/user/whoami.ts @@ -170,7 +170,7 @@ function printTokenPermissions(user: UserInfo) { user.tokenPermissions?.map((scope) => scope.split(":")) ?? []; if (user.authType !== "OAuth Token") { return void logger.log( - `🔓 To see token permissions visit https://dash.cloudflare.com/${user.authType === "User API Token" ? "profile" : user.accounts[0].id}/api-tokens.` + `🔓 To see token permissions, visit https://dash.cloudflare.com/${user.authType === "User API Token" ? "profile" : user.accounts[0].id}/api-tokens` ); } logger.log(`🔓 Token Permissions:`); From 1103c07646569208c4b0a623d123395643e022d5 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 12:20:34 +0100 Subject: [PATCH 5/9] Bump `rosie-skills` from `0.7.6` to `0.8.1` and bundle it into the Wrangler output (#14125) Co-authored-by: Matthew Phillips --- .changeset/angry-carrots-switch.md | 9 ++++ packages/wrangler/package.json | 2 +- packages/wrangler/scripts/deps.ts | 4 -- .../__tests__/agents-skills-install.test.ts | 8 +++- .../wrangler/src/agents-skills-install.ts | 7 ++- pnpm-lock.yaml | 48 ++++++------------- pnpm-workspace.yaml | 4 ++ 7 files changed, 37 insertions(+), 45 deletions(-) create mode 100644 .changeset/angry-carrots-switch.md diff --git a/.changeset/angry-carrots-switch.md b/.changeset/angry-carrots-switch.md new file mode 100644 index 0000000000..a4bac445cd --- /dev/null +++ b/.changeset/angry-carrots-switch.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +Bump `rosie-skills` from `0.7.6` to `0.8.1` and bundle it into the Wrangler output + +The new version of `rosie-skills` is a [pure-TypeScript rewrite](https://github.com/withastro/rosie/pull/21) that removes the previously necessary ~600kb WASM binary. The package now ships only JavaScript with one minimal dependencies (`modern-tar`). + +Additionally, `rosie-skills` is now bundled directly into Wrangler's distributable rather than kept as an external runtime dependency. This eliminates the supply chain concern raised in [#14110](https://github.com/cloudflare/workers-sdk/issues/14110): there is no separate package to resolve at install time, since all code is inlined into Wrangler's build output. diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 067ae383bf..53f496db87 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -71,7 +71,6 @@ "esbuild": "catalog:default", "miniflare": "workspace:*", "path-to-regexp": "6.3.0", - "rosie-skills": "^0.7.6", "unenv": "2.0.0-rc.24", "workerd": "1.20260529.1" }, @@ -150,6 +149,7 @@ "qr": "^0.6.0", "recast": "0.23.11", "resolve": "^1.22.8", + "rosie-skills": "^0.8.1", "semiver": "^1.1.0", "shell-quote": "^1.8.1", "signal-exit": "catalog:default", diff --git a/packages/wrangler/scripts/deps.ts b/packages/wrangler/scripts/deps.ts index 006f02c570..2f90199540 100644 --- a/packages/wrangler/scripts/deps.ts +++ b/packages/wrangler/scripts/deps.ts @@ -35,10 +35,6 @@ export const EXTERNAL_DEPENDENCIES = [ // workerd contains a native binary, so must be external. Wrangler depends on a pinned version. "workerd", - - // rosie-skills contains inlined WASM that is loaded at runtime via import.meta.url-relative - // path resolution, so it cannot be bundled. - "rosie-skills", ]; /** diff --git a/packages/wrangler/src/__tests__/agents-skills-install.test.ts b/packages/wrangler/src/__tests__/agents-skills-install.test.ts index de1fa34f90..742c840575 100644 --- a/packages/wrangler/src/__tests__/agents-skills-install.test.ts +++ b/packages/wrangler/src/__tests__/agents-skills-install.test.ts @@ -24,8 +24,12 @@ vi.unmock("../agents-skills-install"); vi.mock("am-i-vibing"); // Mock rosie-skills to avoid real network/WASM calls. -const mockRosieInstall = vi.fn(); -const mockRosieAgents = vi.fn(); +// vi.hoisted() is required because vi.mock() factories are hoisted above normal +// variable declarations, so plain `const` variables would still be in the TDZ. +const { mockRosieInstall, mockRosieAgents } = vi.hoisted(() => ({ + mockRosieInstall: vi.fn(), + mockRosieAgents: vi.fn(), +})); vi.mock("rosie-skills", () => ({ install: mockRosieInstall, agents: mockRosieAgents, diff --git a/packages/wrangler/src/agents-skills-install.ts b/packages/wrangler/src/agents-skills-install.ts index 0e0e58bed3..560bc7fb30 100644 --- a/packages/wrangler/src/agents-skills-install.ts +++ b/packages/wrangler/src/agents-skills-install.ts @@ -7,6 +7,7 @@ import { } from "@cloudflare/workers-utils"; import { detectAgenticEnvironment } from "am-i-vibing"; import ci from "ci-info"; +import { install as rosieInstall, agents as rosieAgents } from "rosie-skills"; import { fetch } from "undici"; import { confirm } from "./dialogs"; import isInteractive from "./is-interactive"; @@ -116,9 +117,8 @@ export async function maybeInstallCloudflareSkillsGlobally( } try { - const rosie = await import("rosie-skills"); const agentNames = detectedAgents.map((a) => a.rosie.id); - const { failedAgents } = await rosie.install(SKILLS_REPO, { + const { failedAgents } = await rosieInstall(SKILLS_REPO, { global: true, agent: agentNames, lockfile: false, @@ -733,8 +733,7 @@ export function telemetryCurrentAgentSkillsInstalled(): Promise { - const rosie = await import("rosie-skills"); - const allAgents = await rosie.agents(); + const allAgents = await rosieAgents(); return allAgents .filter( ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 008d6a67c3..0762fa3f24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3988,9 +3988,6 @@ importers: path-to-regexp: specifier: 6.3.0 version: 6.3.0 - rosie-skills: - specifier: ^0.7.6 - version: 0.7.6 unenv: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 @@ -4220,6 +4217,9 @@ importers: resolve: specifier: ^1.22.8 version: 1.22.8 + rosie-skills: + specifier: ^0.8.1 + version: 0.8.1 semiver: specifier: ^1.1.0 version: 1.1.0 @@ -12215,6 +12215,10 @@ packages: resolution: {integrity: sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==} engines: {node: '>= 8'} + modern-tar@0.7.6: + resolution: {integrity: sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==} + engines: {node: '>=18.0.0'} + mongodb-connection-string-url@7.0.1: resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} engines: {node: '>=20.19.0'} @@ -13620,23 +13624,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rosie-skills-darwin-arm64@0.7.6: - resolution: {integrity: sha512-EIOMS53cpOvorb2v2QI40MGdVwLlvsYXRdwKMj0GrEPStfsChlmN3EIy0IqLHFRkLpMUTkqUz46wXftNG/rWyA==} - cpu: [arm64] - os: [darwin] - - rosie-skills-freebsd-x64@0.7.6: - resolution: {integrity: sha512-M26M0QjTbIIpK1G013M5N+3k80sjsVWloC7XowgCVigViojvCfaIbnjCeh+/8GAIx4yYFltf4Fem3GWsyK/17w==} - cpu: [x64] - os: [freebsd] - - rosie-skills-linux-x64@0.7.6: - resolution: {integrity: sha512-jlMa48A9c8XssiiNvBcJ1w8RBOOVbTHYzcRaXmqgO3zFLc0R8VTrIpIfmc7KC8dlL313qYerPCrq6LAtGwD0fQ==} - cpu: [x64] - os: [linux] - - rosie-skills@0.7.6: - resolution: {integrity: sha512-B/3CcJlOH3KPNvbyl20VJJmah1ZLQTPjd2hAFIMs1Rn9KY2QN0GqZr0FJ5V+uFtUg3L6WEDGX/i5phIWRnl+BQ==} + rosie-skills@0.8.1: + resolution: {integrity: sha512-byhpM6YcmlQfjPiOBL+6TGW9Wur3DHH4OcVFcGyRhjxeLLRfZg7dEfI2fFKmc6al/eNEPLQ7pSe+JNETk55Fzw==} engines: {node: '>=18'} hasBin: true @@ -23373,6 +23362,8 @@ snapshots: mock-socket@9.3.1: {} + modern-tar@0.7.6: {} + mongodb-connection-string-url@7.0.1: dependencies: '@types/whatwg-url': 13.0.0 @@ -24946,20 +24937,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 - rosie-skills-darwin-arm64@0.7.6: - optional: true - - rosie-skills-freebsd-x64@0.7.6: - optional: true - - rosie-skills-linux-x64@0.7.6: - optional: true - - rosie-skills@0.7.6: - optionalDependencies: - rosie-skills-darwin-arm64: 0.7.6 - rosie-skills-freebsd-x64: 0.7.6 - rosie-skills-linux-x64: 0.7.6 + rosie-skills@0.8.1: + dependencies: + modern-tar: 0.7.6 rou3@0.7.12: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 33016ea2d5..d3daafb72e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -43,6 +43,10 @@ minimumReleaseAgeExclude: # Platform-specific workerd binaries published in lock-step with workerd. - "@cloudflare/workerd-*" - "@cloudflare/workers-types" + # The below is to install the rosie-skills package and remove the problematic + # wasm module it brings as soon as possible + # TODO(dario): remove in a few days + - "rosie-skills" # ────────────────────────────────────────────────────────────────────────────── # Build scripts From 33c31761a05261559a4dc52182de6c20335a9801 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 1 Jun 2026 13:34:15 +0100 Subject: [PATCH 6/9] [vite-plugin] fix: resolve temp dir to long form in E2E tests (Windows) (#14140) --- packages/vite-plugin-cloudflare/e2e/global-setup.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/vite-plugin-cloudflare/e2e/global-setup.ts b/packages/vite-plugin-cloudflare/e2e/global-setup.ts index 61c7a8f438..f12bf45623 100644 --- a/packages/vite-plugin-cloudflare/e2e/global-setup.ts +++ b/packages/vite-plugin-cloudflare/e2e/global-setup.ts @@ -21,8 +21,14 @@ export default async function ({ provide }: TestProject) { "@cloudflare/vite-plugin" ); - // Create temporary directory to host projects used for testing - const root = await fs.mkdtemp(path.join(os.tmpdir(), "vite-plugin-")); + // Create temporary directory to host projects used for testing. + // On Windows GitHub Actions runners, `os.tmpdir()` returns a path containing + // the 8.3 short name "RUNNER~1". Vite 8.0.16 (and the backports to v6/v7) + // rejects paths containing "~" or ":" as a security fix + // (https://github.com/vitejs/vite/pull/22572, GHSA-fx2h-pf6j-xcff), so we + // resolve the temp dir to its real long-form path first. + const tmpdir = await fs.realpath(os.tmpdir()); + const root = await fs.mkdtemp(path.join(tmpdir, "vite-plugin-")); debuglog("Created temporary directory at " + root); // The type of the provided `root` is defined in the `ProvidedContent` type above. From 0e9b7d4acac4ac5cfde82a5b004a6da8d31d8f5b Mon Sep 17 00:00:00 2001 From: James Anderson Date: Mon, 1 Jun 2026 13:57:25 +0100 Subject: [PATCH 7/9] chore: update bonk to opus 4.8 (#14138) --- .github/opencode.json | 1 + .github/workflows/bonk-pr-review.yml | 5 +++-- .github/workflows/bonk.yml | 5 +++-- .opencode/agents/bonk.md | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/opencode.json b/.github/opencode.json index dd25bb96d9..9b765bb1f0 100644 --- a/.github/opencode.json +++ b/.github/opencode.json @@ -5,6 +5,7 @@ "provider": { "cloudflare-ai-gateway": { "models": { + "anthropic/claude-opus-4-8": {}, "anthropic/claude-sonnet-4-5": {}, "workers-ai/@cf/moonshotai/kimi-k2.6": {} } diff --git a/.github/workflows/bonk-pr-review.yml b/.github/workflows/bonk-pr-review.yml index dfeb0c5413..a19e00b68d 100644 --- a/.github/workflows/bonk-pr-review.yml +++ b/.github/workflows/bonk-pr-review.yml @@ -61,8 +61,9 @@ jobs: CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }} CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }} with: - model: "cloudflare-ai-gateway/anthropic/claude-opus-4-6" + model: "cloudflare-ai-gateway/anthropic/claude-opus-4-8" + variant: "high" forks: "false" permissions: write - opencode_version: "1.4.6" # pin to this version as newer versions are causing ProviderInitError issues + opencode_version: 1.15.13 # pin to this version as certain ones cause ProviderInitError issues prompt: ${{ steps.prompt.outputs.value }} diff --git a/.github/workflows/bonk.yml b/.github/workflows/bonk.yml index 3309116ef0..e26078f475 100644 --- a/.github/workflows/bonk.yml +++ b/.github/workflows/bonk.yml @@ -71,9 +71,10 @@ jobs: CLOUDFLARE_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_NAME }} CLOUDFLARE_API_TOKEN: ${{ secrets.CF_AI_GATEWAY_TOKEN }} with: - model: "cloudflare-ai-gateway/anthropic/claude-opus-4-6" + model: "cloudflare-ai-gateway/anthropic/claude-opus-4-8" + variant: "high" mentions: "/bonk,@ask-bonk" permissions: write agent: bonk - opencode_version: "1.4.6" # pin to this version as newer versions are causing ProviderInitError issues + opencode_version: 1.15.13 # pin to this version as certain ones cause ProviderInitError issues prompt: ${{ steps.prompt.outputs.value }} diff --git a/.opencode/agents/bonk.md b/.opencode/agents/bonk.md index d275cf2e06..45d5fe3f74 100644 --- a/.opencode/agents/bonk.md +++ b/.opencode/agents/bonk.md @@ -1,7 +1,7 @@ --- description: Cloudflare Workers SDK engineer. Triages issues, reviews PRs, and implements fixes. mode: primary -model: anthropic/claude-opus-4-6 +model: anthropic/claude-opus-4-8 temperature: 0.2 --- From 2dffeeb92d4f0b8a4c2c91f9cca7959d1970638a Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 1 Jun 2026 14:07:02 +0100 Subject: [PATCH 8/9] Adapt React Router autoconfig based on `v8_middleware` future flag (#14132) --- .../react-router-autoconfig-v8-middleware.md | 9 + .../config-future-no-middleware.ts | 7 + .../config-middleware-and-split.ts | 8 + .../react-router/config-middleware-false.ts | 7 + .../react-router/config-middleware-true.ts | 7 + .../fixtures/react-router/config-no-future.ts | 4 + .../config-plain-object-middleware.ts | 6 + .../react-router/vite-config-basic.ts | 2 + .../frameworks/react-router.test.ts | 375 +++++++++++++++++ packages/wrangler/src/__tests__/tsconfig.json | 3 +- .../src/autoconfig/frameworks/react-router.ts | 394 +++++++++++++----- 11 files changed, 728 insertions(+), 94 deletions(-) create mode 100644 .changeset/react-router-autoconfig-v8-middleware.md create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-future-no-middleware.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-and-split.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-false.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-true.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-no-future.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-plain-object-middleware.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/vite-config-basic.ts create mode 100644 packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts diff --git a/.changeset/react-router-autoconfig-v8-middleware.md b/.changeset/react-router-autoconfig-v8-middleware.md new file mode 100644 index 0000000000..c4564b2056 --- /dev/null +++ b/.changeset/react-router-autoconfig-v8-middleware.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +Adapt React Router autoconfig based on `v8_middleware` future flag + +The React Router autoconfig (`wrangler setup`) now detects whether `v8_middleware: true` is set in the user's `react-router.config.ts`. When it is, the generated `workers/app.ts` uses a simplified fetch handler without `AppLoadContext` module augmentation, and the generated `app/entry.server.tsx` omits the `_loadContext` parameter. When `v8_middleware` is not set, the existing `AppLoadContext` pattern with `env`/`ctx` params is preserved. + +This avoids breaking projects that use the `v8_middleware` future flag (which changes the context API from `AppLoadContext` to `RouterContextProvider`), while keeping the traditional pattern for projects that haven't opted in. diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-future-no-middleware.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-future-no-middleware.ts new file mode 100644 index 0000000000..844e5078d1 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-future-no-middleware.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_splitRouteModules: true, + }, +} satisfies Config; diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-and-split.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-and-split.ts new file mode 100644 index 0000000000..d4298c7391 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-and-split.ts @@ -0,0 +1,8 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_middleware: true, + v8_splitRouteModules: true, + }, +} satisfies Config; diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-false.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-false.ts new file mode 100644 index 0000000000..4696f3a73c --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-false.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_middleware: false, + }, +} satisfies Config; diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-true.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-true.ts new file mode 100644 index 0000000000..dfc17a9fab --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-middleware-true.ts @@ -0,0 +1,7 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, + future: { + v8_middleware: true, + }, +} satisfies Config; diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-no-future.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-no-future.ts new file mode 100644 index 0000000000..f6df07622c --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-no-future.ts @@ -0,0 +1,4 @@ +import type { Config } from "@react-router/dev/config"; +export default { + ssr: true, +} satisfies Config; diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-plain-object-middleware.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-plain-object-middleware.ts new file mode 100644 index 0000000000..44a6ef49d8 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/config-plain-object-middleware.ts @@ -0,0 +1,6 @@ +export default { + ssr: true, + future: { + v8_middleware: true, + }, +}; diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/vite-config-basic.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/vite-config-basic.ts new file mode 100644 index 0000000000..9e2760ea80 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/fixtures/react-router/vite-config-basic.ts @@ -0,0 +1,2 @@ +import { defineConfig } from "vite"; +export default defineConfig({ plugins: [] }); diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts new file mode 100644 index 0000000000..cb8f11f440 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/react-router.test.ts @@ -0,0 +1,375 @@ +import { existsSync, readFileSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; +import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { beforeEach, describe, it, vi } from "vitest"; +import { + hasV8MiddlewareFlag, + ReactRouter, +} from "../../../autoconfig/frameworks/react-router"; +import * as packagesUtils from "../../../autoconfig/frameworks/utils/packages"; +import { NpmPackageManager } from "../../../package-manager"; + +function fixture(name: string): string { + return readFileSync(join(__dirname, "fixtures/react-router", name), "utf-8"); +} + +vi.mock("../../../autoconfig/frameworks/utils/vite-config", () => ({ + transformViteConfig: vi.fn(), +})); + +vi.mock("../../../autoconfig/frameworks/utils/vite-plugin", () => ({ + installCloudflareVitePlugin: vi.fn(), +})); + +vi.mock("../../../autoconfig/frameworks/utils/packages", () => ({ + getInstalledPackageVersion: vi.fn(), + isPackageInstalled: vi.fn(() => true), +})); + +function getBaseOptions() { + return { + projectPath: process.cwd(), + outputDir: "build/", + workerName: "my-react-router-app", + dryRun: false, + packageManager: NpmPackageManager, + isWorkspaceRoot: false, + }; +} + +function createFramework(version: string): ReactRouter { + vi.mocked(packagesUtils.getInstalledPackageVersion).mockReturnValue(version); + + const framework = new ReactRouter({ + id: "react-router", + name: "React Router", + }); + + framework.validateFrameworkVersion(".", { + name: "react-router", + minimumVersion: "7.0.0", + maximumKnownMajorVersion: "7", + }); + + return framework; +} + +describe("hasV8MiddlewareFlag()", () => { + runInTempDir(); + + it("returns false when no config file exists", ({ expect }) => { + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("returns false when config has no future block", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("returns false when future block does not contain v8_middleware", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-future-no-middleware.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("returns true when v8_middleware is set to true", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(true); + }); + + it("returns false when v8_middleware is set to false", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-false.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("handles plain object export without satisfies", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-plain-object-middleware.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(true); + }); + + it("returns false when config file has syntax errors", async ({ expect }) => { + await writeFile( + resolve("react-router.config.ts"), + "export default { this is not valid syntax" + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(false); + }); + + it("detects v8_middleware inside a `satisfies Config` expression", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-and-split.ts") + ); + expect(hasV8MiddlewareFlag(process.cwd())).toBe(true); + }); +}); + +describe("React Router framework configure()", () => { + runInTempDir(); + + beforeEach(async () => { + vi.spyOn(cliPackages, "installPackages").mockImplementation(async () => {}); + + await mkdir(resolve("app"), { recursive: true }); + await writeFile(resolve("vite.config.ts"), fixture("vite-config-basic.ts")); + }); + + describe("workers/app.ts generation — without v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("creates workers/app.ts with AppLoadContext augmentation", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).toContain("AppLoadContext"); + expect(content).toContain("declare module"); + expect(content).toContain("cloudflare: { env, ctx }"); + expect(content).toContain("async fetch(request, env, ctx)"); + expect(content).toContain("satisfies ExportedHandler"); + }); + }); + + describe("workers/app.ts generation — with v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + }); + + it("creates workers/app.ts without AppLoadContext augmentation", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).not.toContain("AppLoadContext"); + expect(content).not.toContain("declare module"); + expect(content).toContain( + 'import { createRequestHandler } from "react-router"' + ); + expect(content).toContain("requestHandler(request)"); + expect(content).toContain("satisfies ExportedHandler"); + }); + + it("creates workers/app.ts with a simple fetch handler", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("workers/app.ts"), "utf-8"); + expect(content).toContain("async fetch(request)"); + expect(content).not.toContain("env, ctx"); + expect(content).not.toContain("cloudflare: { env, ctx }"); + }); + }); + + describe("entry.server.tsx generation — without v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("creates entry.server.tsx with AppLoadContext", async ({ expect }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).toContain("AppLoadContext"); + expect(content).toContain("_loadContext: AppLoadContext"); + expect(content).toContain( + 'import type { AppLoadContext, EntryContext } from "react-router"' + ); + }); + }); + + describe("entry.server.tsx generation — with v8_middleware", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + }); + + it("creates entry.server.tsx without AppLoadContext", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).not.toContain("AppLoadContext"); + expect(content).not.toContain("_loadContext"); + expect(content).toContain( + 'import type { EntryContext } from "react-router"' + ); + expect(content).toContain("ServerRouter"); + expect(content).toContain("renderToReadableStream"); + }); + }); + + describe("entry.server.tsx — existing file not overwritten", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + }); + + it("does not overwrite existing entry.server.tsx", async ({ expect }) => { + const existingContent = "// existing entry server"; + await mkdir(resolve("app"), { recursive: true }); + await writeFile(resolve("app/entry.server.tsx"), existingContent); + + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("app/entry.server.tsx"), "utf-8"); + expect(content).toBe(existingContent); + }); + }); + + describe("react-router.config.ts transformation", () => { + it("adds v8_viteEnvironmentApi for React Router >= 7.10.0", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + expect(content).toContain("v8_viteEnvironmentApi: true"); + // Should NOT add other v8 flags + expect(content).not.toContain("v8_middleware"); + expect(content).not.toContain("v8_splitRouteModules"); + expect(content).not.toContain("v8_passThroughRequests"); + expect(content).not.toContain("v8_trailingSlashAwareDataRequests"); + }); + + it("uses unstable_viteEnvironmentApi for React Router < 7.10.0", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + + const framework = createFramework("7.9.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + expect(content).toContain("unstable_viteEnvironmentApi: true"); + expect(content).not.toContain("v8_viteEnvironmentApi"); + }); + + it("preserves existing future flags when adding viteEnvironmentApi", async ({ + expect, + }) => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-middleware-true.ts") + ); + + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + const content = readFileSync(resolve("react-router.config.ts"), "utf-8"); + // Existing flag preserved + expect(content).toContain("v8_middleware: true"); + // New flag added + expect(content).toContain("v8_viteEnvironmentApi: true"); + }); + }); + + describe("dry run", () => { + it("does not create files in dry run mode", async ({ expect }) => { + const framework = createFramework("7.16.0"); + const result = await framework.configure({ + ...getBaseOptions(), + dryRun: true, + }); + + expect(result.wranglerConfig).toEqual({ + main: "./workers/app.ts", + }); + expect(existsSync(resolve("workers/app.ts"))).toBe(false); + expect(existsSync(resolve("app/entry.server.tsx"))).toBe(false); + }); + }); + + describe("wrangler config", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("returns wrangler config with main pointing to workers/app.ts", async ({ + expect, + }) => { + const framework = createFramework("7.16.0"); + const result = await framework.configure(getBaseOptions()); + + expect(result.wranglerConfig).toEqual({ + main: "./workers/app.ts", + }); + }); + }); + + describe("package installation", () => { + beforeEach(async () => { + await writeFile( + resolve("react-router.config.ts"), + fixture("config-no-future.ts") + ); + }); + + it("installs isbot as a dev dependency", async ({ expect }) => { + const framework = createFramework("7.16.0"); + await framework.configure(getBaseOptions()); + + expect(cliPackages.installPackages).toHaveBeenCalledWith( + NpmPackageManager.type, + ["isbot"], + expect.objectContaining({ dev: true }) + ); + }); + }); +}); diff --git a/packages/wrangler/src/__tests__/tsconfig.json b/packages/wrangler/src/__tests__/tsconfig.json index 0a6745b4d1..69b91ecf19 100644 --- a/packages/wrangler/src/__tests__/tsconfig.json +++ b/packages/wrangler/src/__tests__/tsconfig.json @@ -5,5 +5,6 @@ "types": ["node"], "jsx": "preserve" }, - "include": ["../*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"] + "include": ["../*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"], + "exclude": ["**/fixtures"] } diff --git a/packages/wrangler/src/autoconfig/frameworks/react-router.ts b/packages/wrangler/src/autoconfig/frameworks/react-router.ts index 18b4f84162..f93531ee61 100644 --- a/packages/wrangler/src/autoconfig/frameworks/react-router.ts +++ b/packages/wrangler/src/autoconfig/frameworks/react-router.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; import { brandColor, dim } from "@cloudflare/cli-shared-helpers/colors"; import { installPackages } from "@cloudflare/cli-shared-helpers/packages"; -import { transformFile } from "@cloudflare/codemod"; +import { parseFile, transformFile } from "@cloudflare/codemod"; import * as recast from "recast"; import semiver from "semiver"; import dedent from "ts-dedent"; @@ -15,23 +15,123 @@ import type { ConfigurationOptions, ConfigurationResults, } from "./framework-class"; +import type { Program } from "esprima"; const b = recast.types.builders; +/** + * Resolves the path to the React Router config file (`react-router.config.ts` or `.js`) + * in the given project directory. + * + * @param projectPath - Absolute path to the project root. + * @returns The resolved config file path, or `null` if neither `.ts` nor `.js` variant exists. + */ +function getReactRouterConfigPath(projectPath: string): string | null { + const filePathTS = path.join(projectPath, "react-router.config.ts"); + const filePathJS = path.join(projectPath, "react-router.config.js"); + + if (existsSync(filePathTS)) { + return filePathTS; + } + + if (existsSync(filePathJS)) { + return filePathJS; + } + + return null; +} + +/** + * Checks whether the user's `react-router.config.ts` (or `.js`) has `v8_middleware: true` + * set in its `future` block. This determines which code pattern to generate: + * - With middleware: simplified fetch handler using `cloudflare:workers` env pattern + * - Without middleware: traditional `AppLoadContext` pattern with `env`/`ctx` params + * + * Parses the config file read-only (via `parseFile`) without writing anything to disk. + * Handles both TS `satisfies`/`as` expressions and plain object exports. + * + * @param projectPath - Absolute path to the project root containing the React Router config file. + * @returns `true` if `future.v8_middleware` is explicitly set to `true`, `false` otherwise + * (including when the config file is missing or has no `future` block). + */ +export function hasV8MiddlewareFlag(projectPath: string): boolean { + const filePath = getReactRouterConfigPath(projectPath); + if (!filePath) { + return false; + } + + let ast: Program | null = null; + try { + ast = parseFile(filePath); + } catch {} + + if (!ast) { + return false; + } + + let found = false; + recast.visit(ast, { + visitExportDefaultDeclaration(n) { + let node: recast.types.namedTypes.ObjectExpression | null = null; + if ( + (n.node.declaration.type === "TSAsExpression" || + n.node.declaration.type === "TSSatisfiesExpression") && + n.node.declaration.expression.type === "ObjectExpression" + ) { + node = n.node.declaration.expression; + } else if (n.node.declaration.type === "ObjectExpression") { + node = n.node.declaration; + } + + if (node) { + const futureProp = node.properties.find( + (p) => + p.type === "ObjectProperty" && + p.key.type === "Identifier" && + p.key.name === "future" && + p.value.type === "ObjectExpression" + ); + if ( + futureProp?.type === "ObjectProperty" && + futureProp.value.type === "ObjectExpression" + ) { + found = futureProp.value.properties.some( + (p) => + p.type === "ObjectProperty" && + p.key.type === "Identifier" && + p.key.name === "v8_middleware" && + p.value.type === "BooleanLiteral" && + p.value.value === true + ); + } + } + return false; + }, + }); + + return found; +} + +/** + * Transforms the user's `react-router.config.ts` (or `.js`) to ensure the + * `future.unstable_viteEnvironmentApi` or `future.v8_viteEnvironmentApi` flag + * is set to `true`. If a `future` block already exists, the flag is added or + * updated in place; otherwise a new `future` block is created. + * + * Supports TS `as` and `satisfies` expressions wrapping the default export object. + * + * @param projectPath - Absolute path to the project root containing the React Router config file. + * @param viteEnvironmentKey - The config property name to set, either + * `"unstable_viteEnvironmentApi"` (React Router < 7.10.0) or `"v8_viteEnvironmentApi"`. + * @throws {Error} If no `react-router.config.ts` or `.js` file exists in the project. + * @throws {Error} If the default export cannot be parsed as an object expression. + */ function transformReactRouterConfig( projectPath: string, viteEnvironmentKey: ReturnType ) { - const filePathTS = path.join(projectPath, `react-router.config.ts`); - const filePathJS = path.join(projectPath, `react-router.config.js`); - - let filePath: string; - - if (existsSync(filePathTS)) { - filePath = filePathTS; - } else if (existsSync(filePathJS)) { - filePath = filePathJS; - } else { + const filePath = getReactRouterConfigPath(projectPath); + if (!filePath) { throw new Error("Could not find React Router config file to modify"); } @@ -127,6 +227,15 @@ function transformReactRouterConfig( }); } +/** + * Returns the correct future flag property name for enabling the Vite Environment API + * based on the installed React Router version. + * + * @param reactRouterVersion - The installed React Router semver version string (e.g. `"7.16.0"`). + * When empty, defaults to the stable `"v8_viteEnvironmentApi"` name. + * @returns `"unstable_viteEnvironmentApi"` for versions before 7.10.0, + * or `"v8_viteEnvironmentApi"` for 7.10.0 and later. + */ function configPropertyName(reactRouterVersion: string) { if (!reactRouterVersion) { return "v8_viteEnvironmentApi"; @@ -140,6 +249,184 @@ function configPropertyName(reactRouterVersion: string) { } } +/** + * Writes the `workers/app.ts` Worker entry point. When `v8_middleware` is enabled, + * generates a simplified fetch handler that delegates directly to the request handler. + * Otherwise generates the traditional pattern with `AppLoadContext` module augmentation + * and explicit `env`/`ctx` forwarding. + * + * @param useMiddlewarePattern - Whether `v8_middleware` is enabled in the user's config. + */ +function writeAppTs(useMiddlewarePattern: boolean) { + if (useMiddlewarePattern) { + writeFileSync( + "workers/app.ts", + dedent /* javascript */ ` + import { createRequestHandler } from "react-router"; + + const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE, + ); + + export default { + async fetch(request) { + return requestHandler(request); + }, + } satisfies ExportedHandler; + ` + ); + return; + } + + writeFileSync( + "workers/app.ts", + dedent /* javascript */ ` + import { createRequestHandler } from "react-router"; + + declare module "react-router" { + export interface AppLoadContext { + cloudflare: { + env: Env; + ctx: ExecutionContext; + }; + } + } + + const requestHandler = createRequestHandler( + () => import("virtual:react-router/server-build"), + import.meta.env.MODE + ); + + export default { + async fetch(request, env, ctx) { + return requestHandler(request, { + cloudflare: { env, ctx }, + }); + }, + } satisfies ExportedHandler; + ` + ); +} + +/** + * Writes `app/entry.server.tsx` if it does not already exist. When `v8_middleware` + * is enabled, the generated file omits the `AppLoadContext` import and `_loadContext` + * parameter. Otherwise uses the traditional signature that includes both. + * + * If the file already exists on disk it is left untouched and a warning is logged. + * + * @param useMiddlewarePattern - Whether `v8_middleware` is enabled in the user's config. + */ +function writeEntryServerTsx(useMiddlewarePattern: boolean) { + if (existsSync("app/entry.server.tsx")) { + logger.warn( + "The file `app/entry.server.tsx` already exists on disk, and so we're not modifying it. This may lead to deployment failures if `app/entry.server.tsx` is not set up correctly." + ); + return; + } + + if (useMiddlewarePattern) { + writeFileSync( + `app/entry.server.tsx`, + dedent /* javascript */ ` + import type { EntryContext } from "react-router"; + import { ServerRouter } from "react-router"; + import { isbot } from "isbot"; + import { renderToReadableStream } from "react-dom/server"; + + export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + ) { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); + } + ` + ); + return; + } + + writeFileSync( + `app/entry.server.tsx`, + dedent /* javascript */ ` + import type { AppLoadContext, EntryContext } from "react-router"; + import { ServerRouter } from "react-router"; + import { isbot } from "isbot"; + import { renderToReadableStream } from "react-dom/server"; + + export default async function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + _loadContext: AppLoadContext + ) { + let shellRendered = false; + const userAgent = request.headers.get("user-agent"); + + const body = await renderToReadableStream( + , + { + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + } + ); + shellRendered = true; + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { + await body.allReady; + } + + responseHeaders.set("Content-Type", "text/html"); + return new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }); + } + ` + ); +} + export class ReactRouter extends Framework { async configure({ dryRun, @@ -148,6 +435,7 @@ export class ReactRouter extends Framework { isWorkspaceRoot, }: ConfigurationOptions): Promise { const viteEnvironmentKey = configPropertyName(this.frameworkVersion); + const useMiddlewarePattern = hasV8MiddlewareFlag(projectPath); if (!dryRun) { await installCloudflareVitePlugin({ packageManager: packageManager.type, @@ -157,34 +445,7 @@ export class ReactRouter extends Framework { mkdirSync("workers"); - writeFileSync( - "workers/app.ts", - dedent /* javascript */ ` - import { createRequestHandler } from "react-router"; - - declare module "react-router" { - export interface AppLoadContext { - cloudflare: { - env: Env; - ctx: ExecutionContext; - }; - } - } - - const requestHandler = createRequestHandler( - () => import("virtual:react-router/server-build"), - import.meta.env.MODE - ); - - export default { - async fetch(request, env, ctx) { - return requestHandler(request, { - cloudflare: { env, ctx }, - }); - }, - } satisfies ExportedHandler; - ` - ); + writeAppTs(useMiddlewarePattern); await installPackages(packageManager.type, ["isbot"], { dev: true, @@ -193,60 +454,7 @@ export class ReactRouter extends Framework { isWorkspaceRoot, }); - if (!existsSync("app/entry.server.tsx")) { - writeFileSync( - `app/entry.server.tsx`, - dedent /* javascript */ ` - import type { AppLoadContext, EntryContext } from "react-router"; - import { ServerRouter } from "react-router"; - import { isbot } from "isbot"; - import { renderToReadableStream } from "react-dom/server"; - - export default async function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - routerContext: EntryContext, - _loadContext: AppLoadContext - ) { - let shellRendered = false; - const userAgent = request.headers.get("user-agent"); - - const body = await renderToReadableStream( - , - { - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - } - ); - shellRendered = true; - - // Ensure requests from bots and SPA Mode renders wait for all content to load before responding - // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation - if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) { - await body.allReady; - } - - responseHeaders.set("Content-Type", "text/html"); - return new Response(body, { - headers: responseHeaders, - status: responseStatusCode, - }); - } - ` - ); - } else { - logger.warn( - "The file `app/entry.server.tsx` already exists on disk, and so we're not modifying it. This may lead to deployment failures if `app/entry.server.tsx` is not set up correctly." - ); - } + writeEntryServerTsx(useMiddlewarePattern); transformViteConfig(projectPath, { viteEnvironmentName: "ssr", From 262dfc2b32531165f94ba87c70ce75fcb1490b61 Mon Sep 17 00:00:00 2001 From: MatinGathani <70268627+matingathani@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:52:36 +0530 Subject: [PATCH 9/9] fix(workflows): prevent SQLITE_TOOBIG crash when a step returns a large Uint8Array (#14134) Co-authored-by: Pete Bacon Darwin --- .../workflows-uint8array-sqlite-toobig.md | 17 +++++++ packages/workflows-shared/src/engine.ts | 20 +++++++- .../workflows-shared/tests/engine.test.ts | 49 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .changeset/workflows-uint8array-sqlite-toobig.md diff --git a/.changeset/workflows-uint8array-sqlite-toobig.md b/.changeset/workflows-uint8array-sqlite-toobig.md new file mode 100644 index 0000000000..21754da21b --- /dev/null +++ b/.changeset/workflows-uint8array-sqlite-toobig.md @@ -0,0 +1,17 @@ +--- +"@cloudflare/workflows-shared": patch +--- + +Fix `wrangler dev` Workflows crashing with `SQLITE_TOOBIG` when a step returns a large `Uint8Array` + +`JSON.stringify` encodes each byte of a `Uint8Array` as a separate numeric key +(`{"0":1,"1":2,...}`), producing a string ~10× larger than the array's byte +length. A 200 KB `Uint8Array` became a ~2 MB JSON string that exceeded SQLite's +blob limit, crashing the Workflow step. The same bytes returned as an +`ArrayBuffer` succeeded because `JSON.stringify(ArrayBuffer)` → `{}`. + +The step log metadata (used by the local Workflows explorer) now stores a +human-readable description for `TypedArray` and `ArrayBuffer` outputs +(`[Uint8Array(200000 bytes)]`) instead of attempting to JSON-serialise the raw +bytes. The actual step value is unaffected — it is preserved in Durable Object +key-value storage via structured clone for replay by subsequent steps. diff --git a/packages/workflows-shared/src/engine.ts b/packages/workflows-shared/src/engine.ts index 67dc3b5a7e..5d024c7289 100644 --- a/packages/workflows-shared/src/engine.ts +++ b/packages/workflows-shared/src/engine.ts @@ -108,6 +108,24 @@ export const DEFAULT_STEP_LIMIT = 10_000; const PAUSE_DATETIME = "PAUSE_DATETIME"; +/** + * JSON.stringify replacer that converts TypedArrays and ArrayBuffers to a + * human-readable description. Without this, JSON.stringify(Uint8Array) encodes + * each byte as a numeric key ({"0":1,"1":2,...}), producing a string ~10x larger + * than byteLength and causing SQLITE_TOOBIG for outputs above ~170 KB. + * The replacer is called recursively by JSON.stringify, so nested binary values + * inside objects or arrays are also handled. + */ +function binaryReplacer(_key: string, value: unknown): unknown { + if (value instanceof ArrayBuffer) { + return `[ArrayBuffer(${value.byteLength} bytes)]`; + } + if (ArrayBuffer.isView(value) && !(value instanceof DataView)) { + return `[${value.constructor.name}(${(value as ArrayBufferView).byteLength} bytes)]`; + } + return value; +} + function isStepSuccessEvent(event: InstanceEvent): boolean { return ( event === InstanceEvent.STEP_SUCCESS || @@ -200,7 +218,7 @@ export class Engine extends DurableObject { event, group, target, - JSON.stringify(metadata) + JSON.stringify(metadata, binaryReplacer) ); // Wake any waiters if this is a terminal step event diff --git a/packages/workflows-shared/tests/engine.test.ts b/packages/workflows-shared/tests/engine.test.ts index 5526431f39..6f280fbe1c 100644 --- a/packages/workflows-shared/tests/engine.test.ts +++ b/packages/workflows-shared/tests/engine.test.ts @@ -457,6 +457,55 @@ describe("Engine", () => { ).toBe(true); }); + it("should complete a step that returns a large Uint8Array without SQLITE_TOOBIG", async ({ + expect, + }) => { + // Regression: JSON.stringify(Uint8Array) encodes each byte as a numeric key, + // producing a string far larger than byteLength. A 200 KB Uint8Array → ~2 MB JSON + // → SQLITE_TOOBIG. writeLog must use a replacer to sanitize TypedArrays. + const instanceId = "LARGE-UINT8ARRAY-RESULT"; + const engineId = env.ENGINE.idFromName(instanceId); + const engineStub = env.ENGINE.get(engineId); + + await runWorkflowAndAwait(instanceId, async (_event, step) => { + await step.do("large-binary-step", async () => { + return new Uint8Array(200_000); // ~200 KB, triggers SQLITE_TOOBIG without fix + }); + }); + + const logs = (await engineStub.readLogs()) as EngineLogs; + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_SUCCESS) + ).toBe(true); + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_FAILURE) + ).toBe(false); + }); + + it("should complete a step that returns an object containing a large Uint8Array without SQLITE_TOOBIG", async ({ + expect, + }) => { + // Regression: nested TypedArrays inside objects also cause SQLITE_TOOBIG without + // the JSON.stringify replacer, since sanitization must be recursive. + const instanceId = "NESTED-UINT8ARRAY-RESULT"; + const engineId = env.ENGINE.idFromName(instanceId); + const engineStub = env.ENGINE.get(engineId); + + await runWorkflowAndAwait(instanceId, async (_event, step) => { + await step.do("nested-binary-step", async () => { + return { payload: new Uint8Array(200_000), label: "test" }; + }); + }); + + const logs = (await engineStub.readLogs()) as EngineLogs; + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_SUCCESS) + ).toBe(true); + expect( + logs.logs.some((val) => val.event === InstanceEvent.WORKFLOW_FAILURE) + ).toBe(false); + }); + describe("step limits", () => { it("should enforce step limit when exceeded", async ({ expect }) => { const stepLimit = 3;