From d042705c7a8715184e6e16d399c17adb958d0e80 Mon Sep 17 00:00:00 2001 From: emily-shen <69125074+emily-shen@users.noreply.github.com> Date: Tue, 26 May 2026 10:20:47 +0100 Subject: [PATCH 1/7] add new deploy-helpers package to be used in cf cli (#14014) --- .changeset/curly-deploy-helpers.md | 7 ++ .changeset/plain-deploy-gate.md | 5 + packages/deploy-helpers/package.json | 52 ++++++++ packages/deploy-helpers/src/index.ts | 1 + packages/deploy-helpers/src/shared/types.ts | 113 ++++++++++++++++++ packages/deploy-helpers/tests/index.test.ts | 7 ++ packages/deploy-helpers/tests/tsconfig.json | 8 ++ packages/deploy-helpers/tsconfig.json | 10 ++ packages/deploy-helpers/tsup.config.ts | 17 +++ packages/deploy-helpers/turbo.json | 16 +++ packages/deploy-helpers/vitest.config.mts | 11 ++ packages/workers-utils/package.json | 2 + packages/workers-utils/src/index.ts | 7 ++ packages/workers-utils/src/types.ts | 62 +++++++++- .../workers-utils/src/wrangler-tmp-dir.ts | 111 +++++++++++++++++ .../tests/wrangler-tmp-dir.test.ts | 72 +++++++++++ packages/wrangler/package.json | 3 +- .../deploy/config-args-merging.test.ts | 8 +- .../wrangler/src/__tests__/get-entry.test.ts | 3 +- packages/wrangler/src/__tests__/paths.test.ts | 73 +---------- .../src/api/integrations/platform/index.ts | 2 +- .../api/startDevWorker/BundlerController.ts | 5 +- .../wrangler/src/api/startDevWorker/types.ts | 2 +- packages/wrangler/src/assets.ts | 16 +-- packages/wrangler/src/check/commands.ts | 3 +- packages/wrangler/src/deploy/deploy.ts | 8 +- .../src/deployment-bundle/apply-middleware.ts | 3 +- .../wrangler/src/deployment-bundle/bundle.ts | 5 +- .../src/deployment-bundle/deploy-args.ts | 21 ++-- .../wrangler/src/deployment-bundle/entry.ts | 27 +---- .../find-additional-modules.ts | 11 +- .../deployment-bundle/module-collection.ts | 2 +- .../src/deployment-bundle/no-bundle-worker.ts | 3 +- packages/wrangler/src/dev/miniflare/index.ts | 4 +- packages/wrangler/src/dev/remote.ts | 4 +- packages/wrangler/src/dev/use-esbuild.ts | 8 +- .../src/pages/functions/buildPlugin.ts | 2 +- .../src/pages/functions/buildWorker.ts | 3 +- packages/wrangler/src/pages/utils.ts | 2 +- packages/wrangler/src/paths.ts | 110 ----------------- packages/wrangler/src/sites.ts | 30 +---- packages/wrangler/src/triggers/deploy.ts | 3 +- packages/wrangler/src/triggers/index.ts | 7 ++ .../wrangler/src/type-generation/helpers.ts | 3 +- .../wrangler/src/type-generation/index.ts | 2 +- .../src/utils/friendly-validator-errors.ts | 3 +- packages/wrangler/src/versions/upload.ts | 11 +- pnpm-lock.yaml | 44 ++++++- pnpm-workspace.yaml | 1 + .../__tests__/validate-changesets.test.ts | 1 + 50 files changed, 633 insertions(+), 301 deletions(-) create mode 100644 .changeset/curly-deploy-helpers.md create mode 100644 .changeset/plain-deploy-gate.md create mode 100644 packages/deploy-helpers/package.json create mode 100644 packages/deploy-helpers/src/index.ts create mode 100644 packages/deploy-helpers/src/shared/types.ts create mode 100644 packages/deploy-helpers/tests/index.test.ts create mode 100644 packages/deploy-helpers/tests/tsconfig.json create mode 100644 packages/deploy-helpers/tsconfig.json create mode 100644 packages/deploy-helpers/tsup.config.ts create mode 100644 packages/deploy-helpers/turbo.json create mode 100644 packages/deploy-helpers/vitest.config.mts create mode 100644 packages/workers-utils/src/wrangler-tmp-dir.ts create mode 100644 packages/workers-utils/tests/wrangler-tmp-dir.test.ts diff --git a/.changeset/curly-deploy-helpers.md b/.changeset/curly-deploy-helpers.md new file mode 100644 index 0000000000..7464ccf0a4 --- /dev/null +++ b/.changeset/curly-deploy-helpers.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/deploy-helpers": minor +--- + +Add `@cloudflare/deploy-helpers` package. + +This introduces a shared internal package for deploy-related helper types and code. diff --git a/.changeset/plain-deploy-gate.md b/.changeset/plain-deploy-gate.md new file mode 100644 index 0000000000..facac9a050 --- /dev/null +++ b/.changeset/plain-deploy-gate.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Add `--x-deploy-helpers` to gate an upcoming deploy path refactor. diff --git a/packages/deploy-helpers/package.json b/packages/deploy-helpers/package.json new file mode 100644 index 0000000000..c90bbc2f64 --- /dev/null +++ b/packages/deploy-helpers/package.json @@ -0,0 +1,52 @@ +{ + "name": "@cloudflare/deploy-helpers", + "version": "0.0.1", + "description": "Internal deploy helpers for workers-sdk. Not intended for external use — APIs may change without notice.", + "homepage": "https://github.com/cloudflare/workers-sdk/tree/main/packages/deploy-helpers#readme", + "bugs": { + "url": "https://github.com/cloudflare/workers-sdk/issues" + }, + "license": "MIT OR Apache-2.0", + "author": "workers-devprod@cloudflare.com", + "repository": { + "type": "git", + "url": "https://github.com/cloudflare/workers-sdk.git", + "directory": "packages/deploy-helpers" + }, + "files": [ + "dist" + ], + "sideEffects": false, + "exports": { + ".": { + "import": "./dist/index.mjs", + "types": "./dist/index.d.mts" + } + }, + "scripts": { + "build": "tsup", + "check:type": "tsc -p ./tsconfig.json", + "deploy": "echo 'no deploy'", + "dev": "concurrently -c black,blue --kill-others-on-fail false \"pnpm tsup --watch src\" \"pnpm run check:type --watch --preserveWatchOutput\"", + "test": "vitest", + "test:ci": "vitest run", + "type:tests": "tsc -p ./tests/tsconfig.json" + }, + "devDependencies": { + "@cloudflare/containers-shared": "workspace:*", + "@cloudflare/workers-tsconfig": "workspace:*", + "@cloudflare/workers-utils": "workspace:*", + "@types/node": "catalog:default", + "concurrently": "^8.2.2", + "miniflare": "workspace:*", + "tsup": "8.3.0", + "typescript": "catalog:default", + "vitest": "catalog:default" + }, + "volta": { + "extends": "../../package.json" + }, + "workers-sdk": { + "prerelease": true + } +} diff --git a/packages/deploy-helpers/src/index.ts b/packages/deploy-helpers/src/index.ts new file mode 100644 index 0000000000..a4a4e0dc33 --- /dev/null +++ b/packages/deploy-helpers/src/index.ts @@ -0,0 +1 @@ +export * from "./shared/types"; diff --git a/packages/deploy-helpers/src/shared/types.ts b/packages/deploy-helpers/src/shared/types.ts new file mode 100644 index 0000000000..6a5e3741b3 --- /dev/null +++ b/packages/deploy-helpers/src/shared/types.ts @@ -0,0 +1,113 @@ +import type { ContainerNormalizedConfig } from "@cloudflare/containers-shared"; +import type { + AssetsOptions, + LegacyAssetPaths, + CfPlacement, + Config, + EphemeralDirectory, + Route, + Entry, +} from "@cloudflare/workers-utils"; +import type { NodeJSCompatMode } from "miniflare"; + +/** + * Shared fields produced by merging CLI args with wrangler config. + * After this point, no raw config/arg merging should happen. + * + * Use props for all resolved/merged values. Only access config directly + * for raw values that aren't merged with CLI args (e.g., config.durable_objects, + * config.unsafe, config.tail_consumers). + */ +export type SharedDeployVersionsProps = { + config: Config; + /** Merged from args.script/config.main/config.site.entry-point/config.assets. */ + entry: Entry; + /** From config.rules. */ + rules: Config["rules"]; + /** Merged: --name arg ?? config.name, with CI override applied. */ + name: string; + workerNameOverridden: boolean; + /** Merged: --compatibility-date arg ?? config.compatibility_date. Still optional — validated as required in stage 4. */ + compatibilityDate: string | undefined; + /** Merged: --compatibility-flags arg ?? config.compatibility_flags. */ + compatibilityFlags: string[]; + /** computed based on compat date and args */ + nodejsCompatMode: NodeJSCompatMode; + /** Merged from --assets arg and config.assets. */ + assetsOptions: AssetsOptions | undefined; + /** Merged: --jsx-factory arg || config.jsx_factory. */ + jsxFactory: string; + /** Merged: --jsx-fragment arg || config.jsx_fragment. */ + jsxFragment: string; + /** Merged: --tsconfig arg ?? config.tsconfig. */ + tsconfig: string | undefined; + /** Merged: --minify arg ?? config.minify. */ + minify: boolean | undefined; + /** Merged: !(--bundle arg ?? !config.no_bundle). */ + noBundle: boolean; + /** Merged: --upload-source-maps arg ?? config.upload_source_maps. */ + uploadSourceMaps: boolean | undefined; + /** Merged: --keep-vars arg || config.keep_vars. */ + keepVars: boolean; + /** Merged from --site arg and config.site. */ + isWorkersSite: boolean; + /** Merged: { ...config.define, ...--define arg }. CLI overrides config. */ + defines: Record; + /** Merged: { ...config.alias, ...--alias arg }. CLI overrides config. */ + alias: Record; + /** + * Whether to use the deprecated service environments API path. + * True only when config opts in (legacy_env: false) AND --env is specified. + */ + useServiceEnvApiPath: boolean; + placement: CfPlacement | undefined; + /** Output directory for the bundled Worker. From --outdir arg or a temp directory. */ + destination: string | EphemeralDirectory; + /** From --dry-run arg. */ + dryRun: boolean; + /** From --env arg. */ + env: string | undefined; + /** From --outdir arg. Already used to derive `destination`, but also needed for outdir README and noBundleWorker. */ + outdir: string | undefined; + /** From --outfile arg. */ + outfile: string | undefined; + /** From --tag arg. */ + tag: string | undefined; + /** From --message arg. */ + message: string | undefined; + /** From --secrets-file arg. */ + secretsFile: string | undefined; + /** From collectKeyValues(--var arg). Pre-resolved key-value pairs. */ + var: Record; + /** From --experimental-auto-create arg. */ + experimentalAutoCreate: boolean; +}; + +export type DeployProps = SharedDeployVersionsProps & { + /** Discriminant for DeployProps vs VersionsUploadProps */ + command: "deploy"; + /** Merged from --site arg and config.site. */ + legacyAssetPaths: LegacyAssetPaths | undefined; + /** Merged: --triggers arg ?? config.triggers.crons. */ + triggers: string[] | undefined; + /** Merged: --routes arg ?? config.routes ?? config.route. AND --domains and custom_domains*/ + routes: Route[]; + /** Merged: --logpush arg ?? config.logpush. */ + logpush: boolean | undefined; + containers: ContainerNormalizedConfig[]; + /** From --dispatch-namespace arg. Deploy-only (Workers for Platforms). */ + dispatchNamespace: string | undefined; + /** From --strict arg. Deploy-only. */ + strict: boolean; + /** From --metafile arg. Deploy-only. */ + metafile: string | boolean | undefined; + /** From --old-asset-ttl arg. Deploy-only. */ + oldAssetTtl: number | undefined; +}; + +export type VersionsUploadProps = SharedDeployVersionsProps & { + /** Discriminant for DeployProps vs VersionsUploadProps */ + command: "versions upload"; + /** CLI-only (--preview-alias), or auto-generated from CI branch name. */ + previewAlias: string | undefined; +}; diff --git a/packages/deploy-helpers/tests/index.test.ts b/packages/deploy-helpers/tests/index.test.ts new file mode 100644 index 0000000000..bb30edb9cf --- /dev/null +++ b/packages/deploy-helpers/tests/index.test.ts @@ -0,0 +1,7 @@ +import { describe, it } from "vitest"; + +describe("placeholder", () => { + it("should pass", ({ expect }) => { + expect(true).toBe(true); + }); +}); diff --git a/packages/deploy-helpers/tests/tsconfig.json b/packages/deploy-helpers/tests/tsconfig.json new file mode 100644 index 0000000000..094c387712 --- /dev/null +++ b/packages/deploy-helpers/tests/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "preserve", + "types": ["node"] + }, + "include": ["../*.d.ts", "**/*.ts", "**/*.js"] +} diff --git a/packages/deploy-helpers/tsconfig.json b/packages/deploy-helpers/tsconfig.json new file mode 100644 index 0000000000..1fd9f9e989 --- /dev/null +++ b/packages/deploy-helpers/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@cloudflare/workers-tsconfig/tsconfig.json", + "compilerOptions": { + "module": "esnext", + "types": ["node"], + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["dist", "node_modules", "**/__tests__/**", "**/*.test.ts"] +} diff --git a/packages/deploy-helpers/tsup.config.ts b/packages/deploy-helpers/tsup.config.ts new file mode 100644 index 0000000000..9c4e367648 --- /dev/null +++ b/packages/deploy-helpers/tsup.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "tsup"; + +export default defineConfig(() => [ + { + treeshake: true, + keepNames: true, + entry: ["src/index.ts"], + platform: "node", + format: "esm", + dts: true, + outDir: "dist", + tsconfig: "tsconfig.json", + metafile: true, + sourcemap: process.env.SOURCEMAPS !== "false", + external: ["@cloudflare/*"], + }, +]); diff --git a/packages/deploy-helpers/turbo.json b/packages/deploy-helpers/turbo.json new file mode 100644 index 0000000000..a90e902eb8 --- /dev/null +++ b/packages/deploy-helpers/turbo.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "build": { + "inputs": ["$TURBO_DEFAULT$", "!**/__tests__/**"], + "outputs": ["dist/**"], + "env": ["SOURCEMAPS"], + "passThroughEnv": ["PWD"] + }, + "test:ci": { + "dependsOn": ["build"], + "env": ["LC_ALL", "TZ"] + } + } +} diff --git a/packages/deploy-helpers/vitest.config.mts b/packages/deploy-helpers/vitest.config.mts new file mode 100644 index 0000000000..d4cc444315 --- /dev/null +++ b/packages/deploy-helpers/vitest.config.mts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + testTimeout: 15_000, + pool: "forks", + include: ["**/tests/**/*.test.ts"], + reporters: ["default"], + mockReset: true, + }, +}); diff --git a/packages/workers-utils/package.json b/packages/workers-utils/package.json index b4be794db8..b9f1bedeca 100644 --- a/packages/workers-utils/package.json +++ b/packages/workers-utils/package.json @@ -50,12 +50,14 @@ "@cloudflare/workflows-shared": "workspace:*", "@types/command-exists": "^1.2.0", "@types/node": "catalog:default", + "@types/signal-exit": "^3.0.1", "@vitest/ui": "catalog:default", "cloudflare": "^5.2.0", "command-exists": "^1.2.9", "concurrently": "^8.2.2", "empathic": "^2.0.0", "jsonc-parser": "catalog:default", + "signal-exit": "catalog:default", "smol-toml": "catalog:default", "ts-dedent": "^2.2.0", "tsdown": "^0.15.9", diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index 366ec3fd4b..405be7b541 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -93,6 +93,13 @@ export { isDockerfile } from "./config/validation"; export { isDirectory, removeDir, removeDirSync } from "./fs-helpers"; +export { + type EphemeralDirectory, + getWranglerHiddenDirPath, + getWranglerTmpDir, + sweepStaleWranglerTmpDirs, +} from "./wrangler-tmp-dir"; + export { MetricsRegistry } from "./prometheus-metrics"; export type { Counter } from "./prometheus-metrics"; diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index aa5882c379..9e80ac9235 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -42,8 +42,9 @@ import type { CfVpcService, CfWorkerLoader, CfWorkflow, + CfScriptFormat, } from "./worker"; -import type { AssetConfig } from "@cloudflare/workers-shared"; +import type { AssetConfig, RouterConfig } from "@cloudflare/workers-shared"; export type Json = | string @@ -203,6 +204,40 @@ export type AssetConfigMetadata = { _headers?: string; }; +export type AssetsOptions = { + directory: string; + binding?: string; + routerConfig: RouterConfig; + assetConfig: AssetConfig; + _redirects?: string; + _headers?: string; + run_worker_first?: boolean | string[]; +}; + +/** + * Information about the assets that should be uploaded + */ +export interface LegacyAssetPaths { + /** + * Absolute path to the root of the project. + * + * This is the directory containing wrangler.toml or cwd if no config. + */ + baseDirectory: string; + /** + * The path to the assets directory, relative to the `baseDirectory`. + */ + assetDirectory: string; + /** + * An array of patterns that match files that should be uploaded. + */ + includePatterns: string[]; + /** + * An array of patterns that match files that should not be uploaded. + */ + excludePatterns: string[]; +} + // for PUT /accounts/:accountId/workers/scripts/:scriptName type WorkerMetadataPut = { /** The name of the entry point module. Only exists when the worker is in the ES module format */ @@ -354,3 +389,28 @@ export type Binding = | ({ type: `unsafe_${string}` } & Omit) | { type: "assets" } | { type: "inherit" }; + +/** + * An entry point for the Worker. + * + * It consists not just of a `file`, but also of a `directory` that is used to resolve relative paths. + */ +export type Entry = { + /** A worker's entrypoint */ + file: string; + /** A worker's directory. Usually where the Wrangler configuration file is located */ + projectRoot: string; + /** The path to the config file, if it exists. */ + configPath: string | undefined; + /** Is this a module worker or a service worker? */ + format: CfScriptFormat; + /** The directory that contains all of a `--no-bundle` worker's modules. Usually `${directory}/src`. Defaults to path.dirname(file) */ + moduleRoot: string; + /** + * A worker's name + */ + name?: string | undefined; + + /** Export from a Worker's entrypoint */ + exports: string[]; +}; diff --git a/packages/workers-utils/src/wrangler-tmp-dir.ts b/packages/workers-utils/src/wrangler-tmp-dir.ts new file mode 100644 index 0000000000..83b1e4d730 --- /dev/null +++ b/packages/workers-utils/src/wrangler-tmp-dir.ts @@ -0,0 +1,111 @@ +import fs from "node:fs"; +import path from "node:path"; +import onExit from "signal-exit"; +import { removeDirSync } from "./fs-helpers"; + +/** + * A short-lived directory. Automatically removed when the process exits, but + * can be removed earlier by calling `remove()`. + */ +export interface EphemeralDirectory { + path: string; + remove(): void; +} + +/** + * Gets the path to the project's `.wrangler` folder. + */ +export function getWranglerHiddenDirPath( + projectRoot: string | undefined +): string { + projectRoot ??= process.cwd(); + return path.join(projectRoot, ".wrangler"); +} + +/** + * Maximum age of a `.wrangler/tmp/*` entry before we treat it as orphaned and + * eligible for the startup sweep. Tuned to avoid touching any directory a + * concurrent wrangler session might still own. + */ +const STALE_WRANGLER_TMP_DIR_MS = 24 * 60 * 60 * 1000; + +/** + * Tracks tmp roots already swept by this process so repeated + * `getWranglerTmpDir` calls within one wrangler invocation only scan once. + */ +const sweptTmpRoots = new Set(); + +/** + * Removes stale `.wrangler/tmp/*` entries left behind by previous wrangler + * sessions that exited abnormally (SIGKILL, OOM, host crash) and so missed + * the `signal-exit` cleanup. Runs at most once per tmp root per process. + * + * Exported for tests. + */ +export function sweepStaleWranglerTmpDirs(tmpRoot: string): void { + if (sweptTmpRoots.has(tmpRoot)) { + return; + } + sweptTmpRoots.add(tmpRoot); + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(tmpRoot, { withFileTypes: true }); + } catch { + return; + } + + const cutoff = Date.now() - STALE_WRANGLER_TMP_DIR_MS; + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + const entryPath = path.join(tmpRoot, entry.name); + try { + if (fs.statSync(entryPath).mtimeMs < cutoff) { + removeDirSync(entryPath); + } + } catch { + /* best effort - another process may have removed it first */ + } + } +} + +/** + * Gets a temporary directory in the project's `.wrangler` folder with the + * specified prefix. We create temporary directories in `.wrangler` as opposed + * to the OS's temporary directory to avoid issues with different drive letters + * on Windows. For example, when `esbuild` outputs a file to a different drive + * than the input sources, the generated source maps are incorrect. + */ +export function getWranglerTmpDir( + projectRoot: string | undefined, + prefix: string, + cleanup = true +): EphemeralDirectory { + const tmpRoot = path.join(getWranglerHiddenDirPath(projectRoot), "tmp"); + fs.mkdirSync(tmpRoot, { recursive: true }); + sweepStaleWranglerTmpDirs(tmpRoot); + + const tmpPrefix = path.join(tmpRoot, `${prefix}-`); + const tmpDir = fs.realpathSync(fs.mkdtempSync(tmpPrefix)); + + const cleanupDir = () => { + if (cleanup) { + try { + removeDirSync(tmpDir); + } catch { + /* best effort */ + } + } + }; + const removeExitListener = onExit(cleanupDir); + + return { + path: tmpDir, + remove() { + removeExitListener(); + cleanupDir(); + }, + }; +} diff --git a/packages/workers-utils/tests/wrangler-tmp-dir.test.ts b/packages/workers-utils/tests/wrangler-tmp-dir.test.ts new file mode 100644 index 0000000000..cc2a4aa45c --- /dev/null +++ b/packages/workers-utils/tests/wrangler-tmp-dir.test.ts @@ -0,0 +1,72 @@ +import fs from "node:fs"; +import * as path from "node:path"; +import { describe, it } from "vitest"; +import { runInTempDir } from "../src/test-helpers"; +import { + getWranglerHiddenDirPath, + sweepStaleWranglerTmpDirs, +} from "../src/wrangler-tmp-dir"; + +describe("getWranglerHiddenDirPath()", () => { + it("should return .wrangler path in project root", ({ expect }) => { + expect(getWranglerHiddenDirPath("/project")).toBe( + path.join("/project", ".wrangler") + ); + }); + + it("should use current working directory when projectRoot is undefined", ({ + expect, + }) => { + expect(getWranglerHiddenDirPath(undefined)).toBe( + path.join(process.cwd(), ".wrangler") + ); + }); +}); + +describe("sweepStaleWranglerTmpDirs()", () => { + runInTempDir(); + + const ageDir = (dir: string, ageMs: number): void => { + const seconds = (Date.now() - ageMs) / 1000; + fs.utimesSync(dir, seconds, seconds); + }; + + it("removes orphaned dirs older than the staleness threshold", ({ + expect, + }) => { + const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp"); + fs.mkdirSync(tmpRoot, { recursive: true }); + const stale = path.join(tmpRoot, "bundle-stale"); + const fresh = path.join(tmpRoot, "bundle-fresh"); + fs.mkdirSync(stale); + fs.mkdirSync(fresh); + ageDir(stale, 2 * 24 * 60 * 60 * 1000); + + sweepStaleWranglerTmpDirs(tmpRoot); + + expect(fs.existsSync(stale)).toBe(false); + expect(fs.existsSync(fresh)).toBe(true); + }); + + it("does not throw when the tmp root is missing", ({ expect }) => { + const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp"); + expect(() => sweepStaleWranglerTmpDirs(tmpRoot)).not.toThrow(); + }); + + it("only sweeps a given root once per process", ({ expect }) => { + const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp"); + fs.mkdirSync(tmpRoot, { recursive: true }); + const orphan = path.join(tmpRoot, "bundle-orphan"); + fs.mkdirSync(orphan); + ageDir(orphan, 2 * 24 * 60 * 60 * 1000); + + sweepStaleWranglerTmpDirs(tmpRoot); + expect(fs.existsSync(orphan)).toBe(false); + + // Recreate an equally stale entry; the cached root must skip rescanning. + fs.mkdirSync(orphan); + ageDir(orphan, 2 * 24 * 60 * 60 * 1000); + sweepStaleWranglerTmpDirs(tmpRoot); + expect(fs.existsSync(orphan)).toBe(true); + }); +}); diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index b11f23fd19..b0f0ad3e95 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -81,6 +81,7 @@ "@cloudflare/cli-shared-helpers": "workspace:*", "@cloudflare/codemod": "workspace:*", "@cloudflare/containers-shared": "workspace:*", + "@cloudflare/deploy-helpers": "workspace:*", "@cloudflare/pages-shared": "workspace:^", "@cloudflare/types": "6.18.4", "@cloudflare/workers-shared": "workspace:*", @@ -151,7 +152,7 @@ "resolve": "^1.22.8", "semiver": "^1.1.0", "shell-quote": "^1.8.1", - "signal-exit": "^3.0.7", + "signal-exit": "catalog:default", "smol-toml": "catalog:default", "source-map": "^0.6.1", "supports-color": "^9.2.2", diff --git a/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts b/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts index d0b1bc9930..dc7b8efb64 100644 --- a/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts +++ b/packages/wrangler/src/__tests__/deploy/config-args-merging.test.ts @@ -34,7 +34,7 @@ import { import { mockGetZoneWorkerRoutes } from "../helpers/mock-zone-routes"; import { createFetchResult, msw } from "../helpers/msw"; import { mswListNewDeploymentsLatestFull } from "../helpers/msw/handlers/versions"; -import { runWrangler } from "../helpers/run-wrangler"; +import { runWrangler as runWranglerBase } from "../helpers/run-wrangler"; import { toString } from "../helpers/serialize-form-data-entry"; import { writeWorkerSource } from "../helpers/write-worker-source"; import { @@ -156,12 +156,16 @@ async function getMetadata(request: Request): Promise { // ─── Test suites ───────────────────────────────────────────────────── -describe("config/args merging", () => { +describe.each([ + { mode: "default", flag: "" }, + { mode: "deploy helpers", flag: " --experimental-deploy-helpers" }, +])("config/args merging ($mode)", ({ flag }) => { runInTempDir(); mockAccountId(); mockApiToken(); const { setIsTTY } = useMockIsTTY(); const std = mockConsoleMethods(); + const runWrangler = (command: string) => runWranglerBase(`${command}${flag}`); beforeEach(() => { vi.stubGlobal("setTimeout", (fn: () => void) => { diff --git a/packages/wrangler/src/__tests__/get-entry.test.ts b/packages/wrangler/src/__tests__/get-entry.test.ts index 6190a68a25..53d7468bff 100644 --- a/packages/wrangler/src/__tests__/get-entry.test.ts +++ b/packages/wrangler/src/__tests__/get-entry.test.ts @@ -1,11 +1,10 @@ import path from "node:path"; -import { defaultWranglerConfig } from "@cloudflare/workers-utils"; +import { defaultWranglerConfig, type Entry } from "@cloudflare/workers-utils"; import { runInTempDir, seed } from "@cloudflare/workers-utils/test-helpers"; import dedent from "ts-dedent"; import { describe, it } from "vitest"; import { getEntry } from "../deployment-bundle/entry"; import { mockConsoleMethods } from "./helpers/mock-console"; -import type { Entry } from "../deployment-bundle/entry"; function normalize(entry: Entry): Entry { const tmpDir = process.cwd(); diff --git a/packages/wrangler/src/__tests__/paths.test.ts b/packages/wrangler/src/__tests__/paths.test.ts index 95d2a3de4c..bf74fe7e87 100644 --- a/packages/wrangler/src/__tests__/paths.test.ts +++ b/packages/wrangler/src/__tests__/paths.test.ts @@ -1,13 +1,6 @@ -import fs from "node:fs"; import * as path from "node:path"; -import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; import { describe, it } from "vitest"; -import { - getBasePath, - getWranglerHiddenDirPath, - readableRelative, - sweepStaleWranglerTmpDirs, -} from "../paths"; +import { getBasePath, readableRelative } from "../paths"; describe("paths", () => { describe("getBasePath()", () => { it("should return the path to the wrangler package", ({ expect }) => { @@ -23,70 +16,6 @@ describe("paths", () => { expect(getBasePath()).toEqual(path.resolve("/foo/bar")); }); }); - - describe("getWranglerHiddenDirPath()", () => { - it("should return .wrangler path in project root", ({ expect }) => { - expect(getWranglerHiddenDirPath("/project")).toBe( - path.join("/project", ".wrangler") - ); - }); - - it("should use current working directory when projectRoot is undefined", ({ - expect, - }) => { - expect(getWranglerHiddenDirPath(undefined)).toBe( - path.join(process.cwd(), ".wrangler") - ); - }); - }); -}); - -describe("sweepStaleWranglerTmpDirs()", () => { - runInTempDir(); - - const ageDir = (dir: string, ageMs: number): void => { - const seconds = (Date.now() - ageMs) / 1000; - fs.utimesSync(dir, seconds, seconds); - }; - - it("removes orphaned dirs older than the staleness threshold", ({ - expect, - }) => { - const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp"); - fs.mkdirSync(tmpRoot, { recursive: true }); - const stale = path.join(tmpRoot, "bundle-stale"); - const fresh = path.join(tmpRoot, "bundle-fresh"); - fs.mkdirSync(stale); - fs.mkdirSync(fresh); - ageDir(stale, 2 * 24 * 60 * 60 * 1000); - - sweepStaleWranglerTmpDirs(tmpRoot); - - expect(fs.existsSync(stale)).toBe(false); - expect(fs.existsSync(fresh)).toBe(true); - }); - - it("does not throw when the tmp root is missing", ({ expect }) => { - const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp"); - expect(() => sweepStaleWranglerTmpDirs(tmpRoot)).not.toThrow(); - }); - - it("only sweeps a given root once per process", ({ expect }) => { - const tmpRoot = path.join(process.cwd(), ".wrangler", "tmp"); - fs.mkdirSync(tmpRoot, { recursive: true }); - const orphan = path.join(tmpRoot, "bundle-orphan"); - fs.mkdirSync(orphan); - ageDir(orphan, 2 * 24 * 60 * 60 * 1000); - - sweepStaleWranglerTmpDirs(tmpRoot); - expect(fs.existsSync(orphan)).toBe(false); - - // Recreate an equally stale entry; the cached root must skip rescanning. - fs.mkdirSync(orphan); - ageDir(orphan, 2 * 24 * 60 * 60 * 1000); - sweepStaleWranglerTmpDirs(tmpRoot); - expect(fs.existsSync(orphan)).toBe(true); - }); }); describe("readableRelative", () => { diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index d1b4180c65..7e10bdfe27 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -27,11 +27,11 @@ import { ExecutionContext } from "./executionContext"; // TODO: import from `@cloudflare/workers-utils` after migrating to `tsdown` // This is a temporary fix to ensure that the types are included in the build output import type { + AssetsOptions, Config, RawConfig, RawEnvironment, } from "../../../../../workers-utils/src"; -import type { AssetsOptions } from "../../../assets"; import type { RemoteProxySession } from "../../remoteBindings"; import type { IncomingRequestCfProperties } from "@cloudflare/workers-types/experimental"; import type { diff --git a/packages/wrangler/src/api/startDevWorker/BundlerController.ts b/packages/wrangler/src/api/startDevWorker/BundlerController.ts index 1968c3cae2..e124e57c1e 100644 --- a/packages/wrangler/src/api/startDevWorker/BundlerController.ts +++ b/packages/wrangler/src/api/startDevWorker/BundlerController.ts @@ -1,6 +1,7 @@ import assert from "node:assert"; import { readFileSync, realpathSync, writeFileSync } from "node:fs"; import path from "node:path"; +import { getWranglerTmpDir } from "@cloudflare/workers-utils"; import { watch } from "chokidar"; import { bundleWorker, shouldCheckFetch } from "../../deployment-bundle/bundle"; import { getBundleType } from "../../deployment-bundle/bundle-type"; @@ -14,17 +15,15 @@ import { getAssetChangeMessage } from "../../dev"; import { runBuild } from "../../dev/use-esbuild"; import { logger } from "../../logger"; import { isNavigatorDefined } from "../../navigator-user-agent"; -import { getWranglerTmpDir } from "../../paths"; import { debounce } from "../../utils/debounce"; import { Controller } from "./BaseController"; import { castErrorCause } from "./events"; import { extractBindingsOfType } from "./utils"; import type { BundleResult } from "../../deployment-bundle/bundle"; -import type { Entry } from "../../deployment-bundle/entry"; import type { EsbuildBundle } from "../../dev/use-esbuild"; -import type { EphemeralDirectory } from "../../paths"; import type { ConfigUpdateEvent } from "./events"; import type { StartDevWorkerOptions } from "./types"; +import type { EphemeralDirectory, Entry } from "@cloudflare/workers-utils"; export class BundlerController extends Controller { #currentBundle?: EsbuildBundle; diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index 2bafe7fba4..ae17bcdf03 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -1,10 +1,10 @@ -import type { AssetsOptions } from "../../assets"; import type { CfAccount } from "../../dev/create-worker-preview"; import type { EsbuildBundle } from "../../dev/use-esbuild"; import type { ConfigController } from "./ConfigController"; import type { DevEnv } from "./DevEnv"; import type { ContainerNormalizedConfig } from "@cloudflare/containers-shared"; import type { + AssetsOptions, BinaryFile, Binding, CfModule, diff --git a/packages/wrangler/src/assets.ts b/packages/wrangler/src/assets.ts index 42b1cd88c0..7780525ddb 100644 --- a/packages/wrangler/src/assets.ts +++ b/packages/wrangler/src/assets.ts @@ -31,7 +31,11 @@ import type { StartDevWorkerOptions } from "./api"; import type { DeployArgs } from "./deploy"; import type { StartDevOptions } from "./dev"; import type { AssetConfig, RouterConfig } from "@cloudflare/workers-shared"; -import type { ComplianceConfig, Config } from "@cloudflare/workers-utils"; +import type { + AssetsOptions, + ComplianceConfig, + Config, +} from "@cloudflare/workers-utils"; export type AssetManifest = { [path: string]: { hash: string; size: number } }; @@ -389,16 +393,6 @@ function getAssetsBasePath( : path.resolve(path.dirname(config.configPath ?? "wrangler.toml")); } -export type AssetsOptions = { - directory: string; - binding?: string; - routerConfig: RouterConfig; - assetConfig: AssetConfig; - _redirects?: string; - _headers?: string; - run_worker_first?: boolean | string[]; -}; - export class NonExistentAssetsDirError extends UserError {} export class NonDirectoryAssetsDirError extends UserError {} diff --git a/packages/wrangler/src/check/commands.ts b/packages/wrangler/src/check/commands.ts index e5b41d0d32..ab90353c89 100644 --- a/packages/wrangler/src/check/commands.ts +++ b/packages/wrangler/src/check/commands.ts @@ -4,7 +4,7 @@ import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { log } from "@cloudflare/cli-shared-helpers"; import { spinnerWhile } from "@cloudflare/cli-shared-helpers/interactive"; -import { UserError } from "@cloudflare/workers-utils"; +import { getWranglerTmpDir, UserError } from "@cloudflare/workers-utils"; import chalk from "chalk"; import { Miniflare } from "miniflare"; import { WebSocket } from "ws"; @@ -16,7 +16,6 @@ import { ModuleTypeToRuleType, } from "../deployment-bundle/module-collection"; import { logger } from "../logger"; -import { getWranglerTmpDir } from "../paths"; import type { Config } from "@cloudflare/workers-utils"; import type { ModuleDefinition } from "miniflare"; import type { FormData, FormDataEntryValue } from "undici"; diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 13bb0e481f..47b37cdc2f 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -12,6 +12,7 @@ import { formatConfigSnippet, getDockerPath, parseNonHyphenedUuid, + getWranglerTmpDir, UserError, } from "@cloudflare/workers-utils"; import PQueue from "p-queue"; @@ -50,7 +51,6 @@ import isInteractive, { isNonInteractiveOrCI } from "../is-interactive"; import { logger } from "../logger"; import { getMetricsUsageHeaders } from "../metrics"; import { isNavigatorDefined } from "../navigator-user-agent"; -import { getWranglerTmpDir } from "../paths"; import { ensureQueuesExistByConfig, getQueue, @@ -80,19 +80,19 @@ import { checkRemoteSecretsOverride } from "./check-remote-secrets-override"; import { checkWorkflowConflicts } from "./check-workflow-conflicts"; import { getConfigPatch, getRemoteConfigDiff } from "./config-diffs"; import type { StartDevWorkerInput } from "../api/startDevWorker/types"; -import type { AssetsOptions } from "../assets"; -import type { Entry } from "../deployment-bundle/entry"; import type { PostTypedConsumerBody } from "../queues/client"; -import type { LegacyAssetPaths } from "../sites"; import type { RetrieveSourceMapFunction } from "../sourcemap"; import type { ApiVersion, Percentage, VersionId } from "../versions/types"; import type { + AssetsOptions, CfModule, CfScriptFormat, CfWorkerInit, ComplianceConfig, Config, CustomDomainRoute, + Entry, + LegacyAssetPaths, RawConfig, Route, ZoneIdRoute, diff --git a/packages/wrangler/src/deployment-bundle/apply-middleware.ts b/packages/wrangler/src/deployment-bundle/apply-middleware.ts index 5dba0fe181..da3d3c3de0 100644 --- a/packages/wrangler/src/deployment-bundle/apply-middleware.ts +++ b/packages/wrangler/src/deployment-bundle/apply-middleware.ts @@ -2,8 +2,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { getBasePath } from "../paths"; import { dedent } from "../utils/dedent"; -import type { Entry } from "./entry"; -import type { CfScriptFormat } from "@cloudflare/workers-utils"; +import type { CfScriptFormat, Entry } from "@cloudflare/workers-utils"; /** * A facade that acts as a "middleware loader". diff --git a/packages/wrangler/src/deployment-bundle/bundle.ts b/packages/wrangler/src/deployment-bundle/bundle.ts index 3e1a4a2a45..98b5561913 100644 --- a/packages/wrangler/src/deployment-bundle/bundle.ts +++ b/packages/wrangler/src/deployment-bundle/bundle.ts @@ -3,12 +3,13 @@ import * as path from "node:path"; import { getBuildConditionsFromEnv, getBuildPlatformFromEnv, + getWranglerTmpDir, UserError, } from "@cloudflare/workers-utils"; import chalk from "chalk"; import * as esbuild from "esbuild"; import { getFlag } from "../experimental-flags"; -import { getBasePath, getWranglerTmpDir } from "../paths"; +import { getBasePath } from "../paths"; import { applyMiddlewareLoaderFacade } from "./apply-middleware"; import { isBuildFailure, @@ -23,13 +24,13 @@ import { getNodeJSCompatPlugins } from "./esbuild-plugins/nodejs-plugins"; import { writeAdditionalModules } from "./find-additional-modules"; import { noopModuleCollector } from "./module-collection"; import type { MiddlewareLoader } from "./apply-middleware"; -import type { Entry } from "./entry"; import type { ModuleCollector } from "./module-collection"; import type { CfModule, CfModuleType, Config, DurableObjectBindings, + Entry, WorkflowBinding, } from "@cloudflare/workers-utils"; import type { NodeJSCompatMode } from "miniflare"; diff --git a/packages/wrangler/src/deployment-bundle/deploy-args.ts b/packages/wrangler/src/deployment-bundle/deploy-args.ts index a9bfc67689..857da54ddc 100644 --- a/packages/wrangler/src/deployment-bundle/deploy-args.ts +++ b/packages/wrangler/src/deployment-bundle/deploy-args.ts @@ -146,13 +146,6 @@ export const sharedDeployVersionsArgs = { "Compile a project and run checks without actually uploading the Worker", type: "boolean", }, - "experimental-auto-create": { - describe: "Automatically provision draft bindings with new resources", - type: "boolean", - default: true, - hidden: true, - alias: "x-auto-create", - }, "secrets-file": { describe: "Path to a file containing secrets to upload with the version (JSON or .env format). Applies additively with secrets from previous deployments - omitted secrets will not be deleted.", @@ -168,6 +161,20 @@ export const sharedDeployVersionsArgs = { default: false, type: "boolean", }, + "experimental-auto-create": { + describe: "Automatically provision draft bindings with new resources", + type: "boolean", + default: true, + hidden: true, + alias: "x-auto-create", + }, + "experimental-deploy-helpers": { + describe: "Experimental: Gates refactored deploy/upload path", + type: "boolean", + default: false, + hidden: true, + alias: ["x-deploy-helpers"], + }, } as const satisfies NamedArgDefinitions; export function validateDeployVersionsArgs(args: { diff --git a/packages/wrangler/src/deployment-bundle/entry.ts b/packages/wrangler/src/deployment-bundle/entry.ts index 73c512c057..06aa4e8a9c 100644 --- a/packages/wrangler/src/deployment-bundle/entry.ts +++ b/packages/wrangler/src/deployment-bundle/entry.ts @@ -16,37 +16,12 @@ import { } from "./resolve-entry"; import { runCustomBuild } from "./run-custom-build"; import type { - CfScriptFormat, Config, DurableObjectBindings, + Entry, RawConfig, } from "@cloudflare/workers-utils"; -/** - * An entry point for the Worker. - * - * It consists not just of a `file`, but also of a `directory` that is used to resolve relative paths. - */ -export type Entry = { - /** A worker's entrypoint */ - file: string; - /** A worker's directory. Usually where the Wrangler configuration file is located */ - projectRoot: string; - /** The path to the config file, if it exists. */ - configPath: string | undefined; - /** Is this a module worker or a service worker? */ - format: CfScriptFormat; - /** The directory that contains all of a `--no-bundle` worker's modules. Usually `${directory}/src`. Defaults to path.dirname(file) */ - moduleRoot: string; - /** - * A worker's name - */ - name?: string | undefined; - - /** Export from a Worker's entrypoint */ - exports: string[]; -}; - /** * Compute the entry-point for the Worker. */ diff --git a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts index 9c325f1aa3..00bec40761 100644 --- a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts +++ b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts @@ -1,18 +1,21 @@ import { existsSync } from "node:fs"; import { mkdir, readdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -import { UserError } from "@cloudflare/workers-utils"; +import { getWranglerHiddenDirPath, UserError } from "@cloudflare/workers-utils"; import chalk from "chalk"; import globToRegExp from "glob-to-regexp"; import { logger } from "../logger"; -import { getWranglerHiddenDirPath } from "../paths"; import { getBundleType } from "./bundle-type"; import { RuleTypeToModuleType } from "./module-collection"; import { parseRules } from "./rules"; import { tryAttachSourcemapToModule } from "./source-maps"; -import type { Entry } from "./entry"; import type { ParsedRules } from "./rules"; -import type { CfModule, CfModuleType, Rule } from "@cloudflare/workers-utils"; +import type { + Entry, + CfModule, + CfModuleType, + Rule, +} from "@cloudflare/workers-utils"; async function* getFiles( configPath: string | undefined, diff --git a/packages/wrangler/src/deployment-bundle/module-collection.ts b/packages/wrangler/src/deployment-bundle/module-collection.ts index e89d462587..bb796b3304 100644 --- a/packages/wrangler/src/deployment-bundle/module-collection.ts +++ b/packages/wrangler/src/deployment-bundle/module-collection.ts @@ -12,8 +12,8 @@ import { findAdditionalModuleWatchDirs, } from "./find-additional-modules"; import { isJavaScriptModuleRule, parseRules } from "./rules"; -import type { Entry } from "./entry"; import type { + Entry, CfModule, CfModuleType, Config, diff --git a/packages/wrangler/src/deployment-bundle/no-bundle-worker.ts b/packages/wrangler/src/deployment-bundle/no-bundle-worker.ts index 5c3110d2ae..abc0a1b455 100644 --- a/packages/wrangler/src/deployment-bundle/no-bundle-worker.ts +++ b/packages/wrangler/src/deployment-bundle/no-bundle-worker.ts @@ -3,8 +3,7 @@ import { findAdditionalModules, writeAdditionalModules, } from "./find-additional-modules"; -import type { Entry } from "./entry"; -import type { Rule } from "@cloudflare/workers-utils"; +import type { Rule, Entry } from "@cloudflare/workers-utils"; export async function noBundleWorker( entry: Entry, diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 1818dd3c3d..eac03bfd3a 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -20,11 +20,10 @@ import { updateCheck } from "../../update-check"; import { warnOrError } from "../../utils/print-bindings"; import { getDurableObjectClassNameToUseSQLiteMap } from "../class-names-sqlite"; import type { StartDevWorkerInput } from "../../api/startDevWorker/types"; -import type { AssetsOptions } from "../../assets"; import type { LoggerLevel } from "../../logger"; -import type { LegacyAssetPaths } from "../../sites"; import type { EsbuildBundle } from "../use-esbuild"; import type { + AssetsOptions, Binding, CfD1Database, CfDispatchNamespace, @@ -37,6 +36,7 @@ import type { CfWorkflow, Config, ContainerEngine, + LegacyAssetPaths, } from "@cloudflare/workers-utils"; import type { DOContainerOptions, diff --git a/packages/wrangler/src/dev/remote.ts b/packages/wrangler/src/dev/remote.ts index 602d0adc36..0dbda2ca04 100644 --- a/packages/wrangler/src/dev/remote.ts +++ b/packages/wrangler/src/dev/remote.ts @@ -12,17 +12,17 @@ import { requireApiToken } from "../user"; import { isAbortError } from "../utils/isAbortError"; import { getZoneIdForPreview } from "../zones"; import type { StartDevWorkerInput } from "../api"; -import type { AssetsOptions } from "../assets"; -import type { LegacyAssetPaths } from "../sites"; import type { ApiCredentials } from "../user"; import type { CfAccount } from "./create-worker-preview"; import type { EsbuildBundle } from "./use-esbuild"; import type { + AssetsOptions, CfModule, CfScriptFormat, CfWorkerContext, CfWorkerInit, ComplianceConfig, + LegacyAssetPaths, Route, } from "@cloudflare/workers-utils"; diff --git a/packages/wrangler/src/dev/use-esbuild.ts b/packages/wrangler/src/dev/use-esbuild.ts index 7df6298ed9..5b40945ef3 100644 --- a/packages/wrangler/src/dev/use-esbuild.ts +++ b/packages/wrangler/src/dev/use-esbuild.ts @@ -13,8 +13,12 @@ import { noopModuleCollector, } from "../deployment-bundle/module-collection"; import type { SourceMapMetadata } from "../deployment-bundle/bundle"; -import type { Entry } from "../deployment-bundle/entry"; -import type { CfModule, CfModuleType, Config } from "@cloudflare/workers-utils"; +import type { + CfModule, + CfModuleType, + Config, + Entry, +} from "@cloudflare/workers-utils"; import type { Metafile } from "esbuild"; import type { NodeJSCompatMode } from "miniflare"; diff --git a/packages/wrangler/src/pages/functions/buildPlugin.ts b/packages/wrangler/src/pages/functions/buildPlugin.ts index cb1281f61f..877ef25ce1 100644 --- a/packages/wrangler/src/pages/functions/buildPlugin.ts +++ b/packages/wrangler/src/pages/functions/buildPlugin.ts @@ -5,8 +5,8 @@ import { createModuleCollector } from "../../deployment-bundle/module-collection import { getBasePath } from "../../paths"; import { getPagesProjectRoot } from "../utils"; import { buildNotifierPlugin } from "./buildWorker"; -import type { Entry } from "../../deployment-bundle/entry"; import type { Options as WorkerOptions } from "./buildWorker"; +import type { Entry } from "@cloudflare/workers-utils"; type Options = Omit< WorkerOptions, diff --git a/packages/wrangler/src/pages/functions/buildWorker.ts b/packages/wrangler/src/pages/functions/buildWorker.ts index c5bc86c7bd..b2c2ca525a 100644 --- a/packages/wrangler/src/pages/functions/buildWorker.ts +++ b/packages/wrangler/src/pages/functions/buildWorker.ts @@ -13,8 +13,7 @@ import { logBuildFailure, logger } from "../../logger"; import { getBasePath } from "../../paths"; import { getPagesProjectRoot, getPagesTmpDir } from "../utils"; import type { BundleResult } from "../../deployment-bundle/bundle"; -import type { Entry } from "../../deployment-bundle/entry"; -import type { CfModule } from "@cloudflare/workers-utils"; +import type { CfModule, Entry } from "@cloudflare/workers-utils"; import type { Plugin } from "esbuild"; import type { NodeJSCompatMode } from "miniflare"; diff --git a/packages/wrangler/src/pages/utils.ts b/packages/wrangler/src/pages/utils.ts index e14afaa2cf..b8738cbf64 100644 --- a/packages/wrangler/src/pages/utils.ts +++ b/packages/wrangler/src/pages/utils.ts @@ -1,6 +1,6 @@ import path from "node:path"; +import { getWranglerTmpDir } from "@cloudflare/workers-utils"; import * as find from "empathic/find"; -import { getWranglerTmpDir } from "../paths"; import type { BundleResult } from "../deployment-bundle/bundle"; export const RUNNING_BUILDERS: BundleResult[] = []; diff --git a/packages/wrangler/src/paths.ts b/packages/wrangler/src/paths.ts index f822d448cf..1ac2de8802 100644 --- a/packages/wrangler/src/paths.ts +++ b/packages/wrangler/src/paths.ts @@ -1,8 +1,5 @@ import { assert } from "node:console"; -import fs from "node:fs"; import path from "node:path"; -import { removeDirSync } from "@cloudflare/workers-utils"; -import onExit from "signal-exit"; type DiscriminatedPath = string & { _discriminator: Discriminator; @@ -72,110 +69,3 @@ export function getBasePath(): string { // eslint-disable-next-line no-restricted-globals return path.resolve(__dirname, __RELATIVE_PACKAGE_PATH__); } - -/** - * A short-lived directory. Automatically removed when the process exits, but - * can be removed earlier by calling `remove()`. - */ -export interface EphemeralDirectory { - path: string; - remove(): void; -} - -/** - * Gets the path to the project's `.wrangler` folder. - */ -export function getWranglerHiddenDirPath( - projectRoot: string | undefined -): string { - projectRoot ??= process.cwd(); - return path.join(projectRoot, ".wrangler"); -} - -/** - * Maximum age of a `.wrangler/tmp/*` entry before we treat it as orphaned and - * eligible for the startup sweep. Tuned to avoid touching any directory a - * concurrent wrangler session might still own. - */ -const STALE_WRANGLER_TMP_DIR_MS = 24 * 60 * 60 * 1000; - -/** - * Tracks tmp roots already swept by this process so repeated - * `getWranglerTmpDir` calls within one wrangler invocation only scan once. - */ -const sweptTmpRoots = new Set(); - -/** - * Removes stale `.wrangler/tmp/*` entries left behind by previous wrangler - * sessions that exited abnormally (SIGKILL, OOM, host crash) and so missed - * the `signal-exit` cleanup. Runs at most once per tmp root per process. - * - * Exported for tests. - */ -export function sweepStaleWranglerTmpDirs(tmpRoot: string): void { - if (sweptTmpRoots.has(tmpRoot)) { - return; - } - sweptTmpRoots.add(tmpRoot); - - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(tmpRoot, { withFileTypes: true }); - } catch { - return; - } - - const cutoff = Date.now() - STALE_WRANGLER_TMP_DIR_MS; - for (const entry of entries) { - if (!entry.isDirectory()) { - continue; - } - const entryPath = path.join(tmpRoot, entry.name); - try { - if (fs.statSync(entryPath).mtimeMs < cutoff) { - removeDirSync(entryPath); - } - } catch { - /* best effort — another process may have removed it first */ - } - } -} - -/** - * Gets a temporary directory in the project's `.wrangler` folder with the - * specified prefix. We create temporary directories in `.wrangler` as opposed - * to the OS's temporary directory to avoid issues with different drive letters - * on Windows. For example, when `esbuild` outputs a file to a different drive - * than the input sources, the generated source maps are incorrect. - */ -export function getWranglerTmpDir( - projectRoot: string | undefined, - prefix: string, - cleanup = true -): EphemeralDirectory { - const tmpRoot = path.join(getWranglerHiddenDirPath(projectRoot), "tmp"); - fs.mkdirSync(tmpRoot, { recursive: true }); - sweepStaleWranglerTmpDirs(tmpRoot); - - const tmpPrefix = path.join(tmpRoot, `${prefix}-`); - const tmpDir = fs.realpathSync(fs.mkdtempSync(tmpPrefix)); - - const cleanupDir = () => { - if (cleanup) { - try { - removeDirSync(tmpDir); - } catch { - /* best effort */ - } - } - }; - const removeExitListener = onExit(cleanupDir); - - return { - path: tmpDir, - remove() { - removeExitListener(); - cleanupDir(); - }, - }; -} diff --git a/packages/wrangler/src/sites.ts b/packages/wrangler/src/sites.ts index f34c18fa6b..c109fdd7a0 100644 --- a/packages/wrangler/src/sites.ts +++ b/packages/wrangler/src/sites.ts @@ -18,7 +18,11 @@ import { } from "./kv/helpers"; import { logger, LOGGER_LEVELS } from "./logger"; import type { KeyValue } from "./kv/helpers"; -import type { ComplianceConfig, Config } from "@cloudflare/workers-utils"; +import type { + ComplianceConfig, + Config, + LegacyAssetPaths, +} from "@cloudflare/workers-utils"; import type { XXHashAPI } from "xxhash-wasm"; /** Paths to always ignore. */ @@ -441,30 +445,6 @@ function urlSafe(filePath: string): string { return filePath.replace(/\\/g, "/"); } -/** - * Information about the assets that should be uploaded - */ -export interface LegacyAssetPaths { - /** - * Absolute path to the root of the project. - * - * This is the directory containing wrangler.toml or cwd if no config. - */ - baseDirectory: string; - /** - * The path to the assets directory, relative to the `baseDirectory`. - */ - assetDirectory: string; - /** - * An array of patterns that match files that should be uploaded. - */ - includePatterns: string[]; - /** - * An array of patterns that match files that should not be uploaded. - */ - excludePatterns: string[]; -} - /** * Get an object that describes what site assets to upload, if any. * diff --git a/packages/wrangler/src/triggers/deploy.ts b/packages/wrangler/src/triggers/deploy.ts index 3db53ec49c..2dec82f7c9 100644 --- a/packages/wrangler/src/triggers/deploy.ts +++ b/packages/wrangler/src/triggers/deploy.ts @@ -19,9 +19,8 @@ import { ensureQueuesExistByConfig } from "../queues/client"; import { getWorkersDevSubdomain } from "../routes"; import { retryOnAPIFailure } from "../utils/retry"; import { getZoneForRoute } from "../zones"; -import type { AssetsOptions } from "../assets"; import type { RouteObject } from "../deploy/deploy"; -import type { Config, Route } from "@cloudflare/workers-utils"; +import type { AssetsOptions, Config, Route } from "@cloudflare/workers-utils"; type Props = { config: Config; diff --git a/packages/wrangler/src/triggers/index.ts b/packages/wrangler/src/triggers/index.ts index 7c83015870..01141ad89b 100644 --- a/packages/wrangler/src/triggers/index.ts +++ b/packages/wrangler/src/triggers/index.ts @@ -50,6 +50,13 @@ export const triggersDeployCommand = createCommand({ describe: "Use legacy environments", hidden: true, }, + "experimental-deploy-helpers": { + describe: "Experimental: Gates refactored deploy/upload path", + type: "boolean", + default: false, + hidden: true, + alias: ["x-deploy-helpers"], + }, }, behaviour: { warnIfMultipleEnvsConfiguredButNoneSpecified: true, diff --git a/packages/wrangler/src/type-generation/helpers.ts b/packages/wrangler/src/type-generation/helpers.ts index e6f66c2983..abf0b7dc0b 100644 --- a/packages/wrangler/src/type-generation/helpers.ts +++ b/packages/wrangler/src/type-generation/helpers.ts @@ -10,8 +10,7 @@ import { getEntry } from "../deployment-bundle/entry"; import { logger } from "../logger"; import { generateRuntimeTypes } from "./runtime"; import { generateEnvTypes } from "."; -import type { Entry } from "../deployment-bundle/entry"; -import type { Config } from "@cloudflare/workers-utils"; +import type { Config, Entry } from "@cloudflare/workers-utils"; export const DEFAULT_WORKERS_TYPES_FILE_NAME = "worker-configuration.d.ts"; export const DEFAULT_WORKERS_TYPES_FILE_PATH = `./${DEFAULT_WORKERS_TYPES_FILE_NAME}`; diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 0eb549b291..c371d77c1e 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -31,9 +31,9 @@ import { import { fetchPipelineTypes } from "./pipeline-schema"; import { generateRuntimeTypes } from "./runtime"; import { logRuntimeTypesMessage } from "./runtime/log-runtime-types-message"; -import type { Entry } from "../deployment-bundle/entry"; import type { Config, + Entry, RawConfig, RawEnvironment, } from "@cloudflare/workers-utils"; diff --git a/packages/wrangler/src/utils/friendly-validator-errors.ts b/packages/wrangler/src/utils/friendly-validator-errors.ts index 14a5c4175d..1b22e84c7a 100644 --- a/packages/wrangler/src/utils/friendly-validator-errors.ts +++ b/packages/wrangler/src/utils/friendly-validator-errors.ts @@ -1,10 +1,9 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; -import { ParseError } from "@cloudflare/workers-utils"; +import { getWranglerTmpDir, ParseError } from "@cloudflare/workers-utils"; import dedent from "ts-dedent"; import { analyseBundle } from "../check/commands"; import { logger } from "../logger"; -import { getWranglerTmpDir } from "../paths"; import type { Metafile } from "esbuild"; import type { FormData } from "undici"; diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index f9fce15fac..e68554b0c7 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -11,6 +11,7 @@ import { getCIGeneratePreviewAlias, getCIOverrideName, getWorkersCIBranchName, + getWranglerTmpDir, ParseError, UserError, } from "@cloudflare/workers-utils"; @@ -57,7 +58,6 @@ import { getMetricsUsageHeaders } from "../metrics"; import * as metrics from "../metrics"; import { isNavigatorDefined } from "../navigator-user-agent"; import { writeOutput } from "../output"; -import { getWranglerTmpDir } from "../paths"; import { ensureQueuesExistByConfig } from "../queues/client"; import { getWorkersDevSubdomain } from "../routes"; import { parseBulkInputToObject } from "../secret"; @@ -76,10 +76,13 @@ import { retryOnAPIFailure } from "../utils/retry"; import { useServiceEnvironments } from "../utils/useServiceEnvironments"; import { isWorkerNotFoundError } from "../utils/worker-not-found-error"; import { patchNonVersionedScriptSettings } from "./api"; -import type { AssetsOptions } from "../assets"; -import type { Entry } from "../deployment-bundle/entry"; import type { RetrieveSourceMapFunction } from "../sourcemap"; -import type { CfWorkerInit, Config } from "@cloudflare/workers-utils"; +import type { + AssetsOptions, + CfWorkerInit, + Config, + Entry, +} from "@cloudflare/workers-utils"; import type { FormData } from "undici"; type Props = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acded701f8..d26ca5a7b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ catalogs: playwright-chromium: specifier: ^1.56.1 version: 1.56.1 + signal-exit: + specifier: ^3.0.7 + version: 3.0.7 smol-toml: specifier: ^1.5.2 version: 1.5.2 @@ -1784,6 +1787,36 @@ importers: specifier: ^17.7.2 version: 17.7.2 + packages/deploy-helpers: + devDependencies: + '@cloudflare/containers-shared': + specifier: workspace:* + version: link:../containers-shared + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../workers-tsconfig + '@cloudflare/workers-utils': + specifier: workspace:* + version: link:../workers-utils + '@types/node': + specifier: ^22.10.1 + version: 22.15.17 + concurrently: + specifier: ^8.2.2 + version: 8.2.2 + miniflare: + specifier: workspace:* + version: link:../miniflare + tsup: + specifier: 8.3.0 + version: 8.3.0(@microsoft/api-extractor@7.52.8(@types/node@22.15.17))(jiti@2.6.1)(postcss@8.5.14)(supports-color@9.2.2)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.1) + typescript: + specifier: catalog:default + version: 5.8.3 + vitest: + specifier: catalog:default + version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.8.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) + packages/devprod-status-bot: devDependencies: '@cloudflare/workers-types': @@ -3849,6 +3882,9 @@ importers: '@types/node': specifier: ^22.10.1 version: 22.15.17 + '@types/signal-exit': + specifier: ^3.0.1 + version: 3.0.1 '@vitest/ui': specifier: catalog:default version: 4.1.0(vitest@4.1.0) @@ -3867,6 +3903,9 @@ importers: jsonc-parser: specifier: catalog:default version: 3.2.0 + signal-exit: + specifier: catalog:default + version: 3.0.7 smol-toml: specifier: catalog:default version: 1.5.2 @@ -3971,6 +4010,9 @@ importers: '@cloudflare/containers-shared': specifier: workspace:* version: link:../containers-shared + '@cloudflare/deploy-helpers': + specifier: workspace:* + version: link:../deploy-helpers '@cloudflare/pages-shared': specifier: workspace:^ version: link:../pages-shared @@ -4182,7 +4224,7 @@ importers: specifier: ^1.8.1 version: 1.8.1 signal-exit: - specifier: ^3.0.7 + specifier: catalog:default version: 3.0.7 smol-toml: specifier: catalog:default diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f86ef8b35f..5fa07e1b4a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -122,6 +122,7 @@ catalog: "capnweb": "^0.5.0" "ci-info": "^4.4.0" "open": "^11.0.0" + "signal-exit": "^3.0.7" # CAUTION: Most usage of @cloudflare/vitest-pool-workers in this monorepo should use workspace:* instead of this catalog version # However, some packages (pages-shared, workers-shared, etc...) need to be tested using vitest-pool-workers but are themselves # ultimately included in vitest-pool-workers (through Wrangler), causing a circular dependency. diff --git a/tools/deployments/__tests__/validate-changesets.test.ts b/tools/deployments/__tests__/validate-changesets.test.ts index b549fde1ac..81ee3c5efb 100644 --- a/tools/deployments/__tests__/validate-changesets.test.ts +++ b/tools/deployments/__tests__/validate-changesets.test.ts @@ -26,6 +26,7 @@ describe("findPackageNames()", () => { "@cloudflare/cli-shared-helpers", "@cloudflare/codemod", "@cloudflare/containers-shared", + "@cloudflare/deploy-helpers", "@cloudflare/devprod-status-bot", "@cloudflare/edge-preview-authenticated-proxy", "@cloudflare/lint-config-shared", From 73f918b18d85b8611693f8c9116cb3c2382f270a Mon Sep 17 00:00:00 2001 From: Sam Scott Date: Tue, 26 May 2026 04:50:30 -0500 Subject: [PATCH 2/7] Update examples/test code to use correct chat completions format (#14017) --- packages/devprod-status-bot/src/index.ts | 11 +++++++---- .../fixtures/remote-bindings-disabled/src/index.ts | 8 ++++---- .../playground/external-workers/src/index.ts | 10 ++++++++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/devprod-status-bot/src/index.ts b/packages/devprod-status-bot/src/index.ts index 461bc61fd5..531b2c5492 100644 --- a/packages/devprod-status-bot/src/index.ts +++ b/packages/devprod-status-bot/src/index.ts @@ -19,13 +19,16 @@ async function getBotMessage(ai: Ai, prompt: string) { role: "user", content: prompt, }, - ] as RoleScopedChatInput[], + ], + } satisfies ChatCompletionsMessagesInput; + const message = (await ai.run("@cf/google/gemma-4-26b-a4b-it", chat)) as { + choices?: { message: { content: string } }[]; }; - const message = await ai.run("@cf/google/gemma-4-26b-a4b-it", chat); - if (!("response" in message)) { + const content = message.choices?.[0]?.message.content; + if (!content) { return "I'm feeling a bit poorly 🥲—try asking me for a message later!"; } - return message.response; + return content; } async function isWranglerTeamMember( diff --git a/packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/src/index.ts b/packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/src/index.ts index fb0fb96afe..9a3bc1ff8c 100644 --- a/packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/src/index.ts +++ b/packages/vite-plugin-cloudflare/e2e/fixtures/remote-bindings-disabled/src/index.ts @@ -1,11 +1,11 @@ export default { async fetch(_req, env) { - const content = await env.AI.run("@cf/google/gemma-4-26b-a4b-it", { - messages: [], - }); + const content = (await env.AI.run("@cf/google/gemma-4-26b-a4b-it", { + messages: [{ role: "user", content: "Hello" }], + })) as { choices: { message: { content: string } }[] }; return Response.json({ - response: content.response, + response: content.choices[0].message.content, }); }, } satisfies ExportedHandler<{ AI: Ai }>; diff --git a/packages/vite-plugin-cloudflare/playground/external-workers/src/index.ts b/packages/vite-plugin-cloudflare/playground/external-workers/src/index.ts index b0faae56a2..cb207f49a0 100644 --- a/packages/vite-plugin-cloudflare/playground/external-workers/src/index.ts +++ b/packages/vite-plugin-cloudflare/playground/external-workers/src/index.ts @@ -17,7 +17,9 @@ export default { async fetch(request: Request, env: Env): Promise { const url = new URL(request.url); const aiResponse = await env.AI.run("@cf/google/gemma-4-26b-a4b-it", { - prompt: "When I say PING, you say PONG. PING", + messages: [ + { role: "user", content: "When I say PING, you say PONG. PING" }, + ], }); if (url.pathname === "/vectorize") { @@ -91,7 +93,11 @@ export default { return new Response( JSON.stringify( - (aiResponse as { response: string }).response.toUpperCase() + ( + aiResponse as { + choices: { message: { content: string } }[]; + } + ).choices[0]?.message.content.toUpperCase() ?? "" ) ); }, From 8b1467ef04da43696e3a79eb881cea2f4df022f6 Mon Sep 17 00:00:00 2001 From: Olga Silva <78314353+pombosilva@users.noreply.github.com> Date: Tue, 26 May 2026 11:17:19 +0100 Subject: [PATCH 3/7] [wrangler] Rename Workflows schedule property to schedules (#14044) --- .changeset/workflows-schedules-rename.md | 9 +++++ .../workers-utils/src/config/environment.ts | 2 +- .../workers-utils/src/config/validation.ts | 28 +++++++------- packages/workers-utils/src/worker.ts | 2 +- .../normalize-and-validate-config.test.ts | 38 +++++++++---------- .../src/__tests__/deploy/workflows.test.ts | 38 +++++++++---------- packages/wrangler/src/dev/miniflare/index.ts | 8 ++-- packages/wrangler/src/triggers/deploy.ts | 10 ++--- 8 files changed, 73 insertions(+), 62 deletions(-) create mode 100644 .changeset/workflows-schedules-rename.md diff --git a/.changeset/workflows-schedules-rename.md b/.changeset/workflows-schedules-rename.md new file mode 100644 index 0000000000..bf8556a4a9 --- /dev/null +++ b/.changeset/workflows-schedules-rename.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +Rename Workflow binding `schedule` property to `schedules` + +The `schedule` property on Workflow bindings introduced in [#13467](https://github.com/cloudflare/workers-sdk/pull/13467) has been renamed to `schedules` to match the control plane API. + +> **Note:** This remains a configuration-only change. Scheduled triggering of Workflow instances is not yet available — adding `schedules` to a Workflow binding will not result in scheduled invocations at this time. diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index c309049a65..6604be73fe 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -729,7 +729,7 @@ export type WorkflowBinding = { steps?: number; }; /** Optional cron schedule(s) for automatically triggering workflow instances */ - schedule?: string | string[]; + schedules?: string | string[]; }; /** diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 410a61b1b3..1709d1ed09 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -2699,36 +2699,38 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => { isValid = false; } - if (hasProperty(value, "schedule") && value.schedule !== undefined) { - if (typeof value.schedule === "string") { - if (value.schedule.length === 0) { + if (hasProperty(value, "schedules") && value.schedules !== undefined) { + if (typeof value.schedules === "string") { + if (value.schedules.length === 0) { diagnostics.errors.push( - `"${field}" bindings "schedule" field must not be an empty string.` + `"${field}" bindings "schedules" field must not be an empty string.` ); isValid = false; } - } else if (Array.isArray(value.schedule)) { - if (value.schedule.length === 0) { + } else if (Array.isArray(value.schedules)) { + if (value.schedules.length === 0) { diagnostics.errors.push( - `"${field}" bindings "schedule" field must not be an empty array.` + `"${field}" bindings "schedules" field must not be an empty array.` ); isValid = false; - } else if (!value.schedule.every((s: unknown) => typeof s === "string")) { + } else if ( + !value.schedules.every((s: unknown) => typeof s === "string") + ) { diagnostics.errors.push( - `"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify( + `"${field}" bindings should, optionally, have a string or array of strings "schedules" field but got ${JSON.stringify( value )}.` ); isValid = false; - } else if (value.schedule.some((s: unknown) => s === "")) { + } else if (value.schedules.some((s: unknown) => s === "")) { diagnostics.errors.push( - `"${field}" bindings "schedule" field must not contain empty strings.` + `"${field}" bindings "schedules" field must not contain empty strings.` ); isValid = false; } } else { diagnostics.errors.push( - `"${field}" bindings should, optionally, have a string or array of strings "schedule" field but got ${JSON.stringify( + `"${field}" bindings should, optionally, have a string or array of strings "schedules" field but got ${JSON.stringify( value )}.` ); @@ -2784,7 +2786,7 @@ const validateWorkflowBinding: ValidatorFn = (diagnostics, field, value) => { "script_name", "remote", "limits", - "schedule", + "schedules", ]); return isValid; diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index f9604abd6d..ea98756dbf 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -192,7 +192,7 @@ export interface CfWorkflow { limits?: { steps?: number; }; - schedule?: string | string[]; + schedules?: string | string[]; } export interface CfQueue { diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index 946e0e8b2c..ad18169be9 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -5204,7 +5204,7 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasWarnings()).toBe(false); }); - it("should accept valid workflow bindings with schedule as a string", ({ + it("should accept valid workflow bindings with schedules as a string", ({ expect, }) => { const { diagnostics } = normalizeAndValidateConfig( @@ -5214,7 +5214,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: "*/5 * * * *", + schedules: "*/5 * * * *", }, ], } as unknown as RawConfig, @@ -5227,7 +5227,7 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasWarnings()).toBe(false); }); - it("should accept valid workflow bindings with schedule as an array of strings", ({ + it("should accept valid workflow bindings with schedules as an array of strings", ({ expect, }) => { const { diagnostics } = normalizeAndValidateConfig( @@ -5237,7 +5237,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: ["*/5 * * * *", "0 9 * * 1"], + schedules: ["*/5 * * * *", "0 9 * * 1"], }, ], } as unknown as RawConfig, @@ -5330,7 +5330,7 @@ describe("normalizeAndValidateConfig()", () => { `); }); - it("should error if schedule has wrong type", ({ expect }) => { + it("should error if schedules has wrong type", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( { workflows: [ @@ -5338,7 +5338,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: 123, + schedules: 123, }, ], } as unknown as RawConfig, @@ -5350,11 +5350,11 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasErrors()).toBe(true); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` "Processing wrangler configuration: - - "workflows[0]" bindings should, optionally, have a string or array of strings "schedule" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","schedule":123}." + - "workflows[0]" bindings should, optionally, have a string or array of strings "schedules" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","schedules":123}." `); }); - it("should error if schedule is an empty string", ({ expect }) => { + it("should error if schedules is an empty string", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( { workflows: [ @@ -5362,7 +5362,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: "", + schedules: "", }, ], } as unknown as RawConfig, @@ -5374,11 +5374,11 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasErrors()).toBe(true); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` "Processing wrangler configuration: - - "workflows[0]" bindings "schedule" field must not be an empty string." + - "workflows[0]" bindings "schedules" field must not be an empty string." `); }); - it("should error if schedule is an empty array", ({ expect }) => { + it("should error if schedules is an empty array", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( { workflows: [ @@ -5386,7 +5386,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: [], + schedules: [], }, ], } as unknown as RawConfig, @@ -5398,11 +5398,11 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasErrors()).toBe(true); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` "Processing wrangler configuration: - - "workflows[0]" bindings "schedule" field must not be an empty array." + - "workflows[0]" bindings "schedules" field must not be an empty array." `); }); - it("should error if schedule is an array containing non-strings", ({ + it("should error if schedules is an array containing non-strings", ({ expect, }) => { const { diagnostics } = normalizeAndValidateConfig( @@ -5412,7 +5412,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: ["*/5 * * * *", 123], + schedules: ["*/5 * * * *", 123], }, ], } as unknown as RawConfig, @@ -5424,11 +5424,11 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasErrors()).toBe(true); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` "Processing wrangler configuration: - - "workflows[0]" bindings should, optionally, have a string or array of strings "schedule" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","schedule":["*/5 * * * *",123]}." + - "workflows[0]" bindings should, optionally, have a string or array of strings "schedules" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","schedules":["*/5 * * * *",123]}." `); }); - it("should error if schedule is an array containing empty strings", ({ + it("should error if schedules is an array containing empty strings", ({ expect, }) => { const { diagnostics } = normalizeAndValidateConfig( @@ -5438,7 +5438,7 @@ describe("normalizeAndValidateConfig()", () => { binding: "MY_WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: ["*/5 * * * *", ""], + schedules: ["*/5 * * * *", ""], }, ], } as unknown as RawConfig, @@ -5450,7 +5450,7 @@ describe("normalizeAndValidateConfig()", () => { expect(diagnostics.hasErrors()).toBe(true); expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` "Processing wrangler configuration: - - "workflows[0]" bindings "schedule" field must not contain empty strings." + - "workflows[0]" bindings "schedules" field must not contain empty strings." `); }); diff --git a/packages/wrangler/src/__tests__/deploy/workflows.test.ts b/packages/wrangler/src/__tests__/deploy/workflows.test.ts index 5d4c1b5dea..2d28b01620 100644 --- a/packages/wrangler/src/__tests__/deploy/workflows.test.ts +++ b/packages/wrangler/src/__tests__/deploy/workflows.test.ts @@ -289,7 +289,7 @@ describe("deploy", () => { expect(std.out).toContain("workflow: my-workflow"); }); - it("should deploy a workflow with schedule", async ({ expect }) => { + it("should deploy a workflow with schedules", async ({ expect }) => { writeWranglerConfig({ main: "index.js", workflows: [ @@ -297,7 +297,7 @@ describe("deploy", () => { binding: "WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: "0 * * * *", + schedules: "0 * * * *", }, ], }); @@ -318,7 +318,7 @@ describe("deploy", () => { expect(body).toEqual({ script_name: "test-name", class_name: "MyWorkflow", - schedule: "0 * * * *", + schedules: "0 * * * *", }); return HttpResponse.json( createFetchResult({ id: "mock-new-workflow-id" }) @@ -344,7 +344,7 @@ describe("deploy", () => { expect(std.out).toContain("workflow: my-workflow"); }); - it("should deploy a workflow with schedule as an array of cron expressions", async ({ + it("should deploy a workflow with schedules as an array of cron expressions", async ({ expect, }) => { writeWranglerConfig({ @@ -354,7 +354,7 @@ describe("deploy", () => { binding: "WORKFLOW", name: "my-workflow", class_name: "MyWorkflow", - schedule: ["0 * * * *", "0 9 * * 1"], + schedules: ["0 * * * *", "0 9 * * 1"], }, ], }); @@ -375,7 +375,7 @@ describe("deploy", () => { expect(body).toEqual({ script_name: "test-name", class_name: "MyWorkflow", - schedule: ["0 * * * *", "0 9 * * 1"], + schedules: ["0 * * * *", "0 9 * * 1"], }); return HttpResponse.json( createFetchResult({ id: "mock-new-workflow-id" }) @@ -401,7 +401,7 @@ describe("deploy", () => { expect(std.out).toContain("workflow: my-workflow"); }); - it("should deploy a workflow with both limits and schedule", async ({ + it("should deploy a workflow with both limits and schedules", async ({ expect, }) => { writeWranglerConfig({ @@ -412,7 +412,7 @@ describe("deploy", () => { name: "my-workflow", class_name: "MyWorkflow", limits: { steps: 5000 }, - schedule: "*/15 * * * *", + schedules: "*/15 * * * *", }, ], }); @@ -434,7 +434,7 @@ describe("deploy", () => { script_name: "test-name", class_name: "MyWorkflow", limits: { steps: 5000 }, - schedule: "*/15 * * * *", + schedules: "*/15 * * * *", }); return HttpResponse.json( createFetchResult({ id: "mock-new-workflow-id" }) @@ -568,7 +568,7 @@ describe("deploy", () => { ); }); - it("should error when deploying a workflow with schedule that references an external script", async ({ + it("should error when deploying a workflow with schedules that references an external script", async ({ expect, }) => { writeWranglerConfig({ @@ -580,7 +580,7 @@ describe("deploy", () => { name: "my-workflow", class_name: "MyWorkflow", script_name: "another-script", - schedule: "0 * * * *", + schedules: "0 * * * *", }, ], }); @@ -606,7 +606,7 @@ describe("deploy", () => { ); await expect(runWrangler("deploy")).rejects.toThrow( - 'Workflow "my-workflow" has "schedule" configured but references external script "another-script"' + 'Workflow "my-workflow" has "schedules" configured but references external script "another-script"' ); }); @@ -749,7 +749,7 @@ describe("deploy", () => { expect(std.out).toContain("Uploaded my-app-staging"); }); - it("should error when script_name matches top-level name but not env-suffixed name and schedule is set", async ({ + it("should error when script_name matches top-level name but not env-suffixed name and schedules is set", async ({ expect, }) => { writeWranglerConfig({ @@ -763,7 +763,7 @@ describe("deploy", () => { name: "my-workflow", class_name: "MyWorkflow", script_name: "my-app", - schedule: "0 * * * *", + schedules: "0 * * * *", }, ], }, @@ -777,11 +777,11 @@ describe("deploy", () => { }); await expect(runWrangler("deploy --env staging")).rejects.toThrow( - 'Workflow "my-workflow" has "schedule" configured but references external script "my-app"' + 'Workflow "my-workflow" has "schedules" configured but references external script "my-app"' ); }); - it("should allow schedule when script_name matches the env-suffixed name", async ({ + it("should allow schedules when script_name matches the env-suffixed name", async ({ expect, }) => { writeWranglerConfig({ @@ -795,7 +795,7 @@ describe("deploy", () => { name: "my-workflow", class_name: "MyWorkflow", script_name: "my-app-staging", - schedule: "0 * * * *", + schedules: "0 * * * *", }, ], }, @@ -818,7 +818,7 @@ describe("deploy", () => { expect(body).toEqual({ script_name: "my-app-staging", class_name: "MyWorkflow", - schedule: "0 * * * *", + schedules: "0 * * * *", }); return HttpResponse.json( createFetchResult({ id: "mock-new-workflow-id" }) @@ -837,7 +837,7 @@ describe("deploy", () => { expect(std.out).toContain("workflow: my-workflow"); }); - it("should deploy external script_name under env without schedule", async ({ + it("should deploy external script_name under env without schedules", async ({ expect, }) => { writeWranglerConfig({ diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index eac03bfd3a..10b2247eb7 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -798,11 +798,11 @@ export function buildMiniflareBindingOptions( { telemetryMessage: "workflow limits on external script" } ); } - if (workflow.schedule) { + if (workflow.schedules) { throw new UserError( - `Workflow "${workflow.name}" has "schedule" configured but references external script "${workflow.script_name}". ` + - `Configure schedule on the worker that defines the workflow.`, - { telemetryMessage: "workflow schedule on external script" } + `Workflow "${workflow.name}" has "schedules" configured but references external script "${workflow.script_name}". ` + + `Configure schedules on the worker that defines the workflow.`, + { telemetryMessage: "workflow schedules on external script" } ); } } diff --git a/packages/wrangler/src/triggers/deploy.ts b/packages/wrangler/src/triggers/deploy.ts index 2dec82f7c9..5b863cdbc9 100644 --- a/packages/wrangler/src/triggers/deploy.ts +++ b/packages/wrangler/src/triggers/deploy.ts @@ -279,13 +279,13 @@ export default async function triggersDeploy( } ); } - if (workflow.schedule) { + if (workflow.schedules) { throw new UserError( - `Workflow "${workflow.name}" has "schedule" configured but references external script "${workflow.script_name}". ` + - `Configure schedule on the worker that defines the workflow.`, + `Workflow "${workflow.name}" has "schedules" configured but references external script "${workflow.script_name}". ` + + `Configure schedules on the worker that defines the workflow.`, { telemetryMessage: - "triggers deploy workflow schedule external script", + "triggers deploy workflow schedules external script", } ); } @@ -302,7 +302,7 @@ export default async function triggersDeploy( script_name: scriptName, class_name: workflow.class_name, ...(workflow.limits && { limits: workflow.limits }), - ...(workflow.schedule && { schedule: workflow.schedule }), + ...(workflow.schedules && { schedules: workflow.schedules }), }), headers: { "Content-Type": "application/json", From c1fd2fd3a41de5ee8e4698814d89429b86c75450 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 May 2026 11:19:44 +0100 Subject: [PATCH 4/7] build(deps): bump the workerd-and-workers-types group across 1 directory with 2 updates (#14003) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Wrangler automated PR updater Co-authored-by: Ben <4991309+NuroDev@users.noreply.github.com> --- .changeset/dependabot-update-14003.md | 12 + packages/miniflare/package.json | 2 +- packages/wrangler/package.json | 2 +- pnpm-lock.yaml | 373 +++++++++++++------------- pnpm-workspace.yaml | 4 +- 5 files changed, 203 insertions(+), 190 deletions(-) create mode 100644 .changeset/dependabot-update-14003.md diff --git a/.changeset/dependabot-update-14003.md b/.changeset/dependabot-update-14003.md new file mode 100644 index 0000000000..f7f71887c9 --- /dev/null +++ b/.changeset/dependabot-update-14003.md @@ -0,0 +1,12 @@ +--- +"miniflare": patch +"wrangler": patch +--- + +Update dependencies of "miniflare", "wrangler" + +The following dependency versions have been updated: + +| Dependency | From | To | +| ---------- | ------------ | ------------ | +| workerd | 1.20260521.1 | 1.20260526.1 | diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index 949ebe5123..c901a52580 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -52,7 +52,7 @@ "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "catalog:default", - "workerd": "1.20260521.1", + "workerd": "1.20260526.1", "ws": "catalog:default", "youch": "4.1.0-beta.10" }, diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index b0f0ad3e95..ac3da1f7bf 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -73,7 +73,7 @@ "path-to-regexp": "6.3.0", "rosie-skills": "^0.6.3", "unenv": "2.0.0-rc.24", - "workerd": "1.20260521.1" + "workerd": "1.20260526.1" }, "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d26ca5a7b9..90e227b5b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,8 +10,8 @@ catalogs: specifier: ^0.13.0 version: 0.13.3 '@cloudflare/workers-types': - specifier: ^4.20260521.1 - version: 4.20260521.1 + specifier: ^4.20260526.1 + version: 4.20260526.1 '@hey-api/openapi-ts': specifier: ^0.94.0 version: 0.94.0 @@ -173,7 +173,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -221,7 +221,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 ts-dedent: specifier: ^2.2.0 version: 2.2.0 @@ -239,7 +239,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -260,7 +260,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -284,7 +284,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -320,7 +320,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 undici: specifier: catalog:default version: 7.24.8 @@ -335,7 +335,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -374,7 +374,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -401,7 +401,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -471,7 +471,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -489,11 +489,11 @@ importers: dependencies: '@sentry/cloudflare': specifier: ^10 - version: 10.50.0(@cloudflare/workers-types@4.20260521.1) + version: 10.50.0(@cloudflare/workers-types@4.20260526.1) devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -524,7 +524,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -552,7 +552,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/node': specifier: ^22.10.1 version: 22.15.17 @@ -582,7 +582,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 undici: specifier: catalog:default version: 7.24.8 @@ -600,7 +600,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/debug': specifier: 4.1.12 version: 4.1.12 @@ -633,7 +633,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -658,7 +658,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -682,7 +682,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -721,7 +721,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -742,7 +742,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -763,7 +763,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -781,7 +781,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 is-odd: specifier: ^3.0.1 version: 3.0.1 @@ -800,7 +800,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@fixture/pages-plugin': specifier: workspace:* version: link:../pages-plugin-example @@ -860,7 +860,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -881,7 +881,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1037,19 +1037,19 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 fixtures/rules-app: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 fixtures/secrets-store: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1076,7 +1076,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/is-even': specifier: ^1.0.2 version: 1.0.2 @@ -1100,7 +1100,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 vitest: specifier: catalog:default version: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -1115,7 +1115,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 esbuild: specifier: catalog:default version: 0.27.3 @@ -1133,7 +1133,7 @@ importers: devDependencies: '@better-auth/stripe': specifier: ^1.4.6 - version: 1.5.4(a19b563f2d07928c2a8ae9aafe44b9fb) + version: 1.5.4(e0ef1f524d17813664ad8526a226ab9e) '@cloudflare/containers': specifier: ^0.2.2 version: 0.2.2 @@ -1142,7 +1142,7 @@ importers: version: link:../../packages/vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@microlabs/otel-cf-workers': specifier: 1.0.0-rc.45 version: 1.0.0-rc.45(@opentelemetry/api@1.9.1) @@ -1157,7 +1157,7 @@ importers: version: 3.2.6 better-auth: specifier: ^1.4.6 - version: 1.5.4(@cloudflare/workers-types@4.20260521.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + version: 1.5.4(@cloudflare/workers-types@4.20260526.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) discord-api-types: specifier: 0.37.98 version: 0.37.98 @@ -1228,7 +1228,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@fixture/shared': specifier: workspace:* version: link:../shared @@ -1283,7 +1283,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -1295,7 +1295,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 miniflare: specifier: workspace:* version: link:../../packages/miniflare @@ -1343,7 +1343,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 run-script-os: specifier: ^1.1.6 version: 1.1.6 @@ -1367,7 +1367,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1388,7 +1388,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1409,7 +1409,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1430,7 +1430,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1451,7 +1451,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 @@ -1484,7 +1484,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/node': specifier: ^22.10.1 version: 22.15.17 @@ -1511,7 +1511,7 @@ importers: version: link:../../packages/workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1529,7 +1529,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -1659,7 +1659,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1821,7 +1821,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@octokit/types': specifier: ^13.8.0 version: 13.8.0 @@ -1842,7 +1842,7 @@ importers: version: link:../vitest-pool-workers '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1866,7 +1866,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -1893,10 +1893,10 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260521.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260526.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -2046,8 +2046,8 @@ importers: specifier: catalog:default version: 7.24.8 workerd: - specifier: 1.20260521.1 - version: 1.20260521.1 + specifier: 1.20260526.1 + version: 1.20260526.1 ws: specifier: catalog:default version: 8.20.1 @@ -2072,7 +2072,7 @@ importers: version: link:../workers-shared '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2238,7 +2238,7 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260521.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260526.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-shared': specifier: workspace:* version: link:../workers-shared @@ -2247,7 +2247,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -2275,7 +2275,7 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2309,7 +2309,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/node': specifier: ^22.10.1 version: 22.15.17 @@ -2333,7 +2333,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 esbuild: specifier: catalog:default version: 0.27.3 @@ -2413,7 +2413,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -2509,7 +2509,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2530,7 +2530,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2551,7 +2551,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2572,7 +2572,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2593,7 +2593,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2614,7 +2614,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2635,7 +2635,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2656,7 +2656,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2677,7 +2677,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2698,7 +2698,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2719,7 +2719,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2740,7 +2740,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2761,7 +2761,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2782,7 +2782,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2803,7 +2803,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2824,7 +2824,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2845,7 +2845,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/mimetext': specifier: ^2.0.4 version: 2.0.4 @@ -2878,7 +2878,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2899,7 +2899,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2920,7 +2920,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2941,7 +2941,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2962,7 +2962,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -2983,7 +2983,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3004,7 +3004,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@playground/main-resolution-package': specifier: file:./package version: file:packages/vite-plugin-cloudflare/playground/main-resolution/package @@ -3028,7 +3028,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/express': specifier: ^5.0.1 version: 5.0.1 @@ -3055,7 +3055,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@playground/module-resolution-excludes': specifier: file:./packages/excludes version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/excludes @@ -3067,7 +3067,7 @@ importers: version: file:packages/vite-plugin-cloudflare/playground/module-resolution/packages/requires '@remix-run/cloudflare': specifier: 2.12.0 - version: 2.12.0(@cloudflare/workers-types@4.20260521.1)(typescript@5.8.3) + version: 2.12.0(@cloudflare/workers-types@4.20260526.1)(typescript@5.8.3) '@types/react': specifier: ^18.3.11 version: 18.3.18 @@ -3100,7 +3100,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3121,7 +3121,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@fixture/shared': specifier: workspace:* version: link:../../../../fixtures/shared @@ -3173,7 +3173,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3194,7 +3194,7 @@ importers: dependencies: partyserver: specifier: ^0.3.3 - version: 0.3.3(@cloudflare/workers-types@4.20260521.1) + version: 0.3.3(@cloudflare/workers-types@4.20260526.1) partysocket: specifier: ^1.1.16 version: 1.1.16(react@19.2.1) @@ -3213,7 +3213,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@tailwindcss/vite': specifier: ^4.2.1 version: 4.2.2(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -3249,7 +3249,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3270,7 +3270,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../../../workers-utils @@ -3310,7 +3310,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3340,7 +3340,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3361,7 +3361,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3389,7 +3389,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/react': specifier: 19.1.0 version: 19.1.0 @@ -3422,7 +3422,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3443,7 +3443,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3464,7 +3464,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3485,7 +3485,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@vitejs/plugin-basic-ssl': specifier: ^2.2.0 version: 2.2.0(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) @@ -3509,7 +3509,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3530,7 +3530,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3551,7 +3551,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3572,7 +3572,7 @@ importers: version: link:../../../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 typescript: specifier: catalog:default version: 5.8.3 @@ -3609,7 +3609,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -3824,13 +3824,13 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260521.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260526.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@sentry/cli': specifier: ^2.37.0 version: 2.41.1(encoding@0.1.13) @@ -3945,13 +3945,13 @@ importers: devDependencies: '@cloudflare/vitest-pool-workers': specifier: catalog:default - version: 0.13.3(@cloudflare/workers-types@4.20260521.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) + version: 0.13.3(@cloudflare/workers-types@4.20260526.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0) '@cloudflare/workers-tsconfig': specifier: workspace:* version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -3992,8 +3992,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: 1.20260521.1 - version: 1.20260521.1 + specifier: 1.20260526.1 + version: 1.20260526.1 devDependencies: '@aws-sdk/client-s3': specifier: ^3.721.0 @@ -4027,7 +4027,7 @@ importers: version: link:../workers-tsconfig '@cloudflare/workers-types': specifier: catalog:default - version: 4.20260521.1 + version: 4.20260526.1 '@cloudflare/workers-utils': specifier: workspace:* version: link:../workers-utils @@ -5195,8 +5195,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260521.1': - resolution: {integrity: sha512-aiNdXmxlhwGjTSajL3I7uQPpN4lAOcXjvg5ZOlJKIywnevr798n9XCS6lvuqgniM3KjurBNWRRypMJntg/eSLg==} + '@cloudflare/workerd-darwin-64@1.20260526.1': + resolution: {integrity: sha512-/pR3GH3gfv0PUp7DjI8v0aAIDOqFwibq4bg5xT7TZgcVdBV/cJQWckdXCMqiRtHiawLwogUX00EIOINkYJ1Zqg==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -5213,8 +5213,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260521.1': - resolution: {integrity: sha512-ikN8aKSi4Ak28ndOkuSO5rq6lmV6wwDQu9F9Vu6J7EkwAOth74J/Hjn4j4EuFceW/npw2Ws0Y/muzA6WKHl4TA==} + '@cloudflare/workerd-darwin-arm64@1.20260526.1': + resolution: {integrity: sha512-rcyu0iANYfaiezKh3Mcao1O4IIgVfQldxduiL5TZT1sP0NIeRY4YReSTrzPxNnXxSYaIqaqRHMcHbUM/ic4knA==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -5231,8 +5231,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260521.1': - resolution: {integrity: sha512-D/gUhvQcG0pJr5aJl6yUoi2JxbFpjVtDq9xUJHPjfkAjL28TUVgCR/e5r8YGirepv4I1DK7ihuii9LZ2GGMJbw==} + '@cloudflare/workerd-linux-64@1.20260526.1': + resolution: {integrity: sha512-5EZAEnlLwa9oGJRo8Nd3iY5Wcd9ROGNNG90xNIGp8MEjj8v2jTn42NC47fCZKFdnLj3+S+vWEhu1x0GVJnALjA==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -5249,8 +5249,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260521.1': - resolution: {integrity: sha512-vhjWPIHenczegTakhRPwEmTeaavCpNqsuo3RlLCkUdU47HrwLvy/4QersGggs4+kF4Do+IE/EznCGyT40xYcLA==} + '@cloudflare/workerd-linux-arm64@1.20260526.1': + resolution: {integrity: sha512-X/YBQXeXFeCN7QTStoWrATEBc9WKl7PIqkw/dQkjyJ72gh3rkLe0+Xkzp3wO7gtxTDQMa7NPGy1W4+sdMf8q1g==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -5267,8 +5267,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260521.1': - resolution: {integrity: sha512-wBolYC/+lnGIEbkkPdzFtjTOWip2uQH6maeAP1ZV0kyxi5SGpsa83+wD5rH5OOle+sHE5qJMdwCKjwRwj+FKJg==} + '@cloudflare/workerd-windows-64@1.20260526.1': + resolution: {integrity: sha512-R+tqpFFdcfZIljx8fIW9rj9fRTtDgfoA2yonsfAGa6e8snrmr+38mdFHtkRC0D3UyZpn/hOtmXiUBfdX2gMR7Q==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -5281,8 +5281,8 @@ packages: react: ^17.0.2 || ^18.2.21 react-dom: ^17.0.2 || ^18.2.21 - '@cloudflare/workers-types@4.20260521.1': - resolution: {integrity: sha512-cerUUdckXWiNm0x0XEIomBkr58xja5hn/1F9wzzDfJTE96L98L/Ra0ToTxKfzsXLklPflOagce8Vhg0vx8Ytgw==} + '@cloudflare/workers-types@4.20260526.1': + resolution: {integrity: sha512-pQZBjD7p6C5R1ZPkSywA2yiZnL/LVfqdPKLQLdbEItro4ekmMuGXQv72vHkHIT3GeZmEgZktMA0JoWn2fBKmXw==} '@codemirror/autocomplete@6.20.0': resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==} @@ -8988,6 +8988,7 @@ packages: '@verdaccio/commons-api@10.2.0': resolution: {integrity: sha512-F/YZANu4DmpcEV0jronzI7v2fGVWkQ5Mwi+bVmV+ACJ+EzR0c9Jbhtbe5QyLUuzR97t8R5E/Xe53O0cc2LukdQ==} engines: {node: '>=8'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. '@verdaccio/config@8.0.0-next-8.7': resolution: {integrity: sha512-pA0WCWvvWY6vPRav+X0EuFmuK6M08zIpRzTKkqSriCWk6JUCZ07TDnN054AS8TSSOy6EaWgHxnUw3nTd34Z4Sg==} @@ -14982,8 +14983,8 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260521.1: - resolution: {integrity: sha512-HzIThcZ0ZVEuzVxpY2IYZ3yssSrTjtrWXAVfmOl5rVwyqcu7aeZXGMiwrEmi9MOcC3wjy+BNv+hFrMMY5OrjQQ==} + workerd@1.20260526.1: + resolution: {integrity: sha512-IHzymht98p10JH1zzwdCpbViAqw97HrwKl7+KfZeASFMsYSrIsAULWdPn0LRC5FTUzBpamLNyKCCKxbgXHgRHQ==} engines: {node: '>=16'} hasBin: true @@ -16005,7 +16006,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.13 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16016,9 +16017,9 @@ snapshots: nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 - '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)': + '@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1)': dependencies: '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16029,50 +16030,50 @@ snapshots: nanostores: 1.1.1 zod: 4.3.6 optionalDependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 - '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': + '@better-auth/drizzle-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': + '@better-auth/kysely-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 kysely: 0.28.11 - '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': + '@better-auth/memory-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 - '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': + '@better-auth/mongo-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 mongodb: 7.1.0 - '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': + '@better-auth/prisma-adapter@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) prisma: 7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3) - '@better-auth/stripe@1.5.4(a19b563f2d07928c2a8ae9aafe44b9fb)': + '@better-auth/stripe@1.5.4(e0ef1f524d17813664ad8526a226ab9e)': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) - better-auth: 1.5.4(@cloudflare/workers-types@4.20260521.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + better-auth: 1.5.4(@cloudflare/workers-types@4.20260526.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0) better-call: 1.3.2(zod@4.3.6) defu: 6.1.4 stripe: 20.4.1(@types/node@22.15.17) zod: 4.3.6 - '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))': + '@better-auth/telemetry@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))': dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 @@ -16637,7 +16638,7 @@ snapshots: lodash.memoize: 4.1.2 marked: 0.3.19 - '@cloudflare/vitest-pool-workers@0.13.3(@cloudflare/workers-types@4.20260521.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0)': + '@cloudflare/vitest-pool-workers@0.13.3(@cloudflare/workers-types@4.20260526.1)(@vitest/runner@4.1.0)(@vitest/snapshot@4.1.0)(vitest@4.1.0)': dependencies: '@vitest/runner': 4.1.0 '@vitest/snapshot': 4.1.0 @@ -16645,7 +16646,7 @@ snapshots: esbuild: 0.27.3 miniflare: 4.20260317.1 vitest: 4.1.0(@opentelemetry/api@1.9.1)(@types/node@22.15.17)(@vitest/ui@4.1.0)(msw@2.12.4(@types/node@22.15.17)(typescript@5.9.3))(vite@8.0.13(@types/node@22.15.17)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.1)) - wrangler: 4.76.0(@cloudflare/workers-types@4.20260521.1) + wrangler: 4.76.0(@cloudflare/workers-types@4.20260526.1) zod: 3.25.76 transitivePeerDependencies: - '@cloudflare/workers-types' @@ -16658,7 +16659,7 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260423.1': optional: true - '@cloudflare/workerd-darwin-64@1.20260521.1': + '@cloudflare/workerd-darwin-64@1.20260526.1': optional: true '@cloudflare/workerd-darwin-arm64@1.20260317.1': @@ -16667,7 +16668,7 @@ snapshots: '@cloudflare/workerd-darwin-arm64@1.20260423.1': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260521.1': + '@cloudflare/workerd-darwin-arm64@1.20260526.1': optional: true '@cloudflare/workerd-linux-64@1.20260317.1': @@ -16676,7 +16677,7 @@ snapshots: '@cloudflare/workerd-linux-64@1.20260423.1': optional: true - '@cloudflare/workerd-linux-64@1.20260521.1': + '@cloudflare/workerd-linux-64@1.20260526.1': optional: true '@cloudflare/workerd-linux-arm64@1.20260317.1': @@ -16685,7 +16686,7 @@ snapshots: '@cloudflare/workerd-linux-arm64@1.20260423.1': optional: true - '@cloudflare/workerd-linux-arm64@1.20260521.1': + '@cloudflare/workerd-linux-arm64@1.20260526.1': optional: true '@cloudflare/workerd-windows-64@1.20260317.1': @@ -16694,7 +16695,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260423.1': optional: true - '@cloudflare/workerd-windows-64@1.20260521.1': + '@cloudflare/workerd-windows-64@1.20260526.1': optional: true '@cloudflare/workers-editor-shared@0.1.1(@cloudflare/style-const@6.1.3(react@19.2.4))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@6.1.3(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -16705,7 +16706,7 @@ snapshots: react-dom: 19.2.4(react@19.2.4) react-split-pane: 0.1.92(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@cloudflare/workers-types@4.20260521.1': {} + '@cloudflare/workers-types@4.20260526.1': {} '@codemirror/autocomplete@6.20.0': dependencies: @@ -18116,7 +18117,7 @@ snapshots: '@prisma/adapter-d1@7.0.1': dependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 '@prisma/driver-adapter-utils': 7.0.1 ky: 1.7.5 @@ -18343,10 +18344,10 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260521.1)(typescript@5.8.3)': + '@remix-run/cloudflare@2.12.0(@cloudflare/workers-types@4.20260526.1)(typescript@5.8.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 '@remix-run/server-runtime': 2.12.0(typescript@5.8.3) optionalDependencies: typescript: 5.8.3 @@ -18876,12 +18877,12 @@ snapshots: - encoding - supports-color - '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@4.20260521.1)': + '@sentry/cloudflare@10.50.0(@cloudflare/workers-types@4.20260526.1)': dependencies: '@opentelemetry/api': 1.9.1 '@sentry/core': 10.50.0 optionalDependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 '@sentry/core@10.50.0': {} @@ -20519,15 +20520,15 @@ snapshots: before-after-hook@2.2.3: {} - better-auth@1.5.4(@cloudflare/workers-types@4.20260521.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): + better-auth@1.5.4(@cloudflare/workers-types@4.20260526.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)))(mongodb@7.1.0)(mysql2@3.15.3)(pg@8.16.3)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.0): dependencies: - '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) - '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) - '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) - '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) - '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) - '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) - '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260521.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)) + '@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@6.2.1)(kysely@0.28.11)(nanostores@1.1.1) + '@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))) + '@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11) + '@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1) + '@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0) + '@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + '@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20260526.1)(better-call@1.3.2(zod@4.3.6))(jose@5.9.3)(kysely@0.28.11)(nanostores@1.1.1)) '@better-auth/utils': 0.3.1 '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.1.1 @@ -20540,7 +20541,7 @@ snapshots: zod: 4.3.6 optionalDependencies: '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) - drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) + drizzle-orm: 0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)) mongodb: 7.1.0 mysql2: 3.15.3 pg: 8.16.3 @@ -21351,9 +21352,9 @@ snapshots: dependencies: wordwrap: 1.0.0 - drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260521.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): + drizzle-orm@0.45.1(@cloudflare/workers-types@4.20260526.1)(@electric-sql/pglite@0.3.2)(@opentelemetry/api@1.9.1)(@prisma/client@7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3))(@types/pg@8.15.4)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.16.3)(postgres@3.4.7)(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3)): optionalDependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 '@electric-sql/pglite': 0.3.2 '@opentelemetry/api': 1.9.1 '@prisma/client': 7.0.1(prisma@7.0.1(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.8.3))(typescript@5.8.3) @@ -23801,9 +23802,9 @@ snapshots: parseurl@1.3.3: {} - partyserver@0.3.3(@cloudflare/workers-types@4.20260521.1): + partyserver@0.3.3(@cloudflare/workers-types@4.20260526.1): dependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 nanoid: 5.1.7 partysocket@1.1.16(react@19.2.1): @@ -26615,15 +26616,15 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260423.1 '@cloudflare/workerd-windows-64': 1.20260423.1 - workerd@1.20260521.1: + workerd@1.20260526.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260521.1 - '@cloudflare/workerd-darwin-arm64': 1.20260521.1 - '@cloudflare/workerd-linux-64': 1.20260521.1 - '@cloudflare/workerd-linux-arm64': 1.20260521.1 - '@cloudflare/workerd-windows-64': 1.20260521.1 + '@cloudflare/workerd-darwin-64': 1.20260526.1 + '@cloudflare/workerd-darwin-arm64': 1.20260526.1 + '@cloudflare/workerd-linux-64': 1.20260526.1 + '@cloudflare/workerd-linux-arm64': 1.20260526.1 + '@cloudflare/workerd-windows-64': 1.20260526.1 - wrangler@4.76.0(@cloudflare/workers-types@4.20260521.1): + wrangler@4.76.0(@cloudflare/workers-types@4.20260526.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260317.1) @@ -26634,7 +26635,7 @@ snapshots: unenv: 2.0.0-rc.24 workerd: 1.20260317.1 optionalDependencies: - '@cloudflare/workers-types': 4.20260521.1 + '@cloudflare/workers-types': 4.20260526.1 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 5fa07e1b4a..6e3a1eb315 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -111,8 +111,8 @@ catalog: "ws": "8.20.1" esbuild: "0.27.3" playwright-chromium: "^1.56.1" - "@cloudflare/workers-types": "^4.20260521.1" - workerd: "1.20260521.1" + "@cloudflare/workers-types": "^4.20260526.1" + workerd: "1.20260526.1" jsonc-parser: "^3.2.0" smol-toml: "^1.5.2" msw: 2.12.4 From c809d30f699eb751d7a288fb65be274850018206 Mon Sep 17 00:00:00 2001 From: James Opstad <13586373+jamesopstad@users.noreply.github.com> Date: Tue, 26 May 2026 14:30:09 +0100 Subject: [PATCH 5/7] Add `assetsOnly`/`devOnly` property to the plugin config (#13985) --- .changeset/rare-ducks-occur.md | 33 +++++ .../playground/__test-utils__/index.ts | 7 +- .../playground/__test-utils__/vite-version.ts | 6 + .../playground/multi-worker/.gitignore | 1 + .../custom-build-app/multi-worker.spec.ts | 4 +- .../dev-only-auxiliary/multi-worker.spec.ts | 29 ++++ .../playground/multi-worker/package.json | 2 + .../vite.config.dev-only-auxiliary.ts | 18 +++ .../assets-only/prerendering.spec.ts | 17 +++ .../prerendering/__tests__/base-tests.ts | 26 ++++ .../__tests__/prerendering.spec.ts | 26 ++-- .../playground/prerendering/package.json | 1 + .../prerendering/vite.config.assets-only.ts | 3 + .../playground/prerendering/vite.config.ts | 121 +++++++++-------- packages/vite-plugin-cloudflare/src/build.ts | 24 +++- .../src/deploy-config.ts | 124 +++++++----------- .../src/plugin-config.ts | 66 +++++++++- .../src/plugins/config.ts | 88 +++++++++++-- .../src/plugins/output-config.ts | 13 +- 19 files changed, 423 insertions(+), 186 deletions(-) create mode 100644 .changeset/rare-ducks-occur.md create mode 100644 packages/vite-plugin-cloudflare/playground/__test-utils__/vite-version.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/dev-only-auxiliary/multi-worker.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.dev-only-auxiliary.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/__tests__/assets-only/prerendering.spec.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/__tests__/base-tests.ts create mode 100644 packages/vite-plugin-cloudflare/playground/prerendering/vite.config.assets-only.ts diff --git a/.changeset/rare-ducks-occur.md b/.changeset/rare-ducks-occur.md new file mode 100644 index 0000000000..f787a9cd54 --- /dev/null +++ b/.changeset/rare-ducks-occur.md @@ -0,0 +1,33 @@ +--- +"@cloudflare/vite-plugin": minor +--- + +Add `assetsOnly` (entry Worker) and `devOnly` (auxiliary Workers) options to the plugin config + +Both options accept a `boolean` or a function that returns a `boolean`. The function is evaluated lazily at build time, allowing frameworks to provide the value after initialization. + +Use `assetsOnly` on the entry Worker to skip building the Worker and instead emit an assets-only Wrangler config to the client output directory. This enables frameworks such as Astro to use the `ssr` environment during development but produce a fully static app for deployment. + +```ts +export default defineConfig({ + plugins: [ + cloudflare({ + assetsOnly: () => isStaticBuild, + }), + ], +}); +``` + +Use `devOnly` on an auxiliary Worker to include it during `vite dev` but skip it at build time. + +```ts +export default defineConfig({ + plugins: [ + cloudflare({ + auxiliaryWorkers: [ + { configPath: "./dev-only-worker/wrangler.jsonc", devOnly: true }, + ], + }), + ], +}); +``` diff --git a/packages/vite-plugin-cloudflare/playground/__test-utils__/index.ts b/packages/vite-plugin-cloudflare/playground/__test-utils__/index.ts index 734ea43792..400d4b6fea 100644 --- a/packages/vite-plugin-cloudflare/playground/__test-utils__/index.ts +++ b/packages/vite-plugin-cloudflare/playground/__test-utils__/index.ts @@ -1,15 +1,10 @@ import fs from "node:fs"; -import semverGte from "semver/functions/gte"; -import { version as viteVersion } from "vite"; import { onTestFinished, test } from "vitest"; import { isWindows } from "../vitest-setup"; export * from "../vitest-setup"; export * from "./responses"; - -export function satisfiesViteVersion(minVersion: string): boolean { - return semverGte(viteVersion, minVersion); -} +export { satisfiesMinimumViteVersion } from "./vite-version"; /** Common options to use with `vi.waitFor()` */ export const WAIT_FOR_OPTIONS = { diff --git a/packages/vite-plugin-cloudflare/playground/__test-utils__/vite-version.ts b/packages/vite-plugin-cloudflare/playground/__test-utils__/vite-version.ts new file mode 100644 index 0000000000..d445edc740 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/__test-utils__/vite-version.ts @@ -0,0 +1,6 @@ +import semverGte from "semver/functions/gte"; +import { version as viteVersion } from "vite"; + +export function satisfiesMinimumViteVersion(minVersion: string): boolean { + return semverGte(viteVersion, minVersion); +} diff --git a/packages/vite-plugin-cloudflare/playground/multi-worker/.gitignore b/packages/vite-plugin-cloudflare/playground/multi-worker/.gitignore index b4e768a46c..57e8cfedd5 100644 --- a/packages/vite-plugin-cloudflare/playground/multi-worker/.gitignore +++ b/packages/vite-plugin-cloudflare/playground/multi-worker/.gitignore @@ -2,3 +2,4 @@ custom-root-output-directory custom-environment-output-directory custom-worker-output-directory custom-build-app-directory +custom-dev-only-directory diff --git a/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/custom-build-app/multi-worker.spec.ts b/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/custom-build-app/multi-worker.spec.ts index e685eb4cad..2fcd62f59b 100644 --- a/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/custom-build-app/multi-worker.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/custom-build-app/multi-worker.spec.ts @@ -2,10 +2,10 @@ import { describe, test } from "vitest"; import { getJsonResponse, isBuild, - satisfiesViteVersion, + satisfiesMinimumViteVersion, } from "../../../__test-utils__"; -describe.runIf(isBuild && satisfiesViteVersion("7.0.0"))( +describe.runIf(isBuild && satisfiesMinimumViteVersion("7.0.0"))( "builds additional Worker environments not built in `builder.buildApp` config", () => { test("returns a response from another Worker", async ({ expect }) => { diff --git a/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/dev-only-auxiliary/multi-worker.spec.ts b/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/dev-only-auxiliary/multi-worker.spec.ts new file mode 100644 index 0000000000..1e4183afd9 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/multi-worker/__tests__/dev-only-auxiliary/multi-worker.spec.ts @@ -0,0 +1,29 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { describe, test } from "vitest"; +import { isBuild, rootDir } from "../../../__test-utils__"; + +describe.runIf(isBuild)("dev-only auxiliary Worker", () => { + test("creates output directory for entry worker only", ({ expect }) => { + expect( + fs.existsSync(path.join(rootDir, "custom-dev-only-directory", "worker_a")) + ).toBe(true); + expect( + fs.existsSync(path.join(rootDir, "custom-dev-only-directory", "worker_b")) + ).toBe(false); + }); + + test("does not include dev-only auxiliary Worker in deploy config", ({ + expect, + }) => { + const deployConfigPath = path.join( + rootDir, + ".wrangler", + "deploy", + "config.json" + ); + const deployConfig = JSON.parse(fs.readFileSync(deployConfigPath, "utf-8")); + + expect(deployConfig.auxiliaryWorkers).toEqual([]); + }); +}); diff --git a/packages/vite-plugin-cloudflare/playground/multi-worker/package.json b/packages/vite-plugin-cloudflare/playground/multi-worker/package.json index 999ca7892f..8d00d02955 100644 --- a/packages/vite-plugin-cloudflare/playground/multi-worker/package.json +++ b/packages/vite-plugin-cloudflare/playground/multi-worker/package.json @@ -7,6 +7,7 @@ "build:custom-output-directories": "vite build -c ./vite.config.custom-output-directories.ts", "build:customize-config": "vite build -c ./vite.config.customize-config.ts", "build:default": "vite build", + "build:dev-only-auxiliary": "vite build -c ./vite.config.dev-only-auxiliary.ts", "build:no-config-file": "vite build -c ./vite.config.no-config-file.ts", "build:with-worker-configs-warning": "vite build -c vite.config.with-worker-configs-warning.ts", "check:type": "tsc --build", @@ -14,6 +15,7 @@ "dev:custom-build-app": "vite dev -c ./vite.config.custom-build-app.ts", "dev:custom-output-directories": "vite dev -c ./vite.config.custom-output-directories.ts", "dev:customize-config": "vite dev -c ./vite.config.customize-config.ts", + "dev:dev-only-auxiliary": "vite dev -c ./vite.config.dev-only-auxiliary.ts", "dev:no-config-file": "vite dev -c ./vite.config.no-config-file.ts", "dev:with-worker-configs-warning": "vite dev -c vite.config.with-worker-configs-warning.ts", "preview": "vite preview" diff --git a/packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.dev-only-auxiliary.ts b/packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.dev-only-auxiliary.ts new file mode 100644 index 0000000000..d468b4f4a5 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/multi-worker/vite.config.dev-only-auxiliary.ts @@ -0,0 +1,18 @@ +import { cloudflare } from "@cloudflare/vite-plugin"; +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + outDir: "custom-dev-only-directory", + }, + plugins: [ + cloudflare({ + configPath: "./worker-a/wrangler.jsonc", + auxiliaryWorkers: [ + { configPath: "./worker-b/wrangler.jsonc", devOnly: true }, + ], + inspectorPort: false, + persistState: false, + }), + ], +}); diff --git a/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/assets-only/prerendering.spec.ts b/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/assets-only/prerendering.spec.ts new file mode 100644 index 0000000000..4eebc09320 --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/assets-only/prerendering.spec.ts @@ -0,0 +1,17 @@ +import { test, describe } from "vitest"; +import "../base-tests"; +import { + satisfiesMinimumViteVersion, + isBuild, + viteTestUrl, +} from "../../../__test-utils__"; + +describe.runIf(satisfiesMinimumViteVersion("7.0.0"))("assets-only", () => { + test.runIf(isBuild)( + "does not return a server rendered response at /hello after the build", + async ({ expect }) => { + const response = await fetch(`${viteTestUrl}/hello`); + expect(response.status).toBe(404); + } + ); +}); diff --git a/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/base-tests.ts b/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/base-tests.ts new file mode 100644 index 0000000000..750e84beaf --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/base-tests.ts @@ -0,0 +1,26 @@ +import { test, describe } from "vitest"; +import { + getTextResponse, + isBuild, + page, + satisfiesMinimumViteVersion, + viteTestUrl, +} from "../../__test-utils__"; + +describe.runIf(satisfiesMinimumViteVersion("7.0.0"))("pre-rendering", () => { + test.runIf(!isBuild)( + "returns the server rendered response at /hello in dev", + async ({ expect }) => { + expect(await getTextResponse("/hello")).toEqual("Hello world"); + } + ); + + test.runIf(isBuild)( + "returns the prerendered route at /prerendered after the build", + async ({ expect }) => { + await page.goto(`${viteTestUrl}/prerendered`); + const content = await page.textContent("h1"); + expect(content).toBe("Pre-rendered HTML"); + } + ); +}); diff --git a/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/prerendering.spec.ts b/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/prerendering.spec.ts index 254bf2b967..ae37e97199 100644 --- a/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/prerendering.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/prerendering/__tests__/prerendering.spec.ts @@ -1,21 +1,17 @@ -import { test } from "vitest"; +import { test, describe } from "vitest"; +import "./base-tests"; import { - getTextResponse, + satisfiesMinimumViteVersion, isBuild, - page, - satisfiesViteVersion, viteTestUrl, } from "../../__test-utils__"; -test("returns the server rendered route at /", async ({ expect }) => { - expect(await getTextResponse()).toEqual("Hello world"); +describe.runIf(satisfiesMinimumViteVersion("7.0.0"))("with-ssr", () => { + test.runIf(isBuild)( + "returns a server rendered response at /hello after the build", + async ({ expect }) => { + const response = await fetch(`${viteTestUrl}/hello`); + expect(response.status).toBe(200); + } + ); }); - -test.runIf(isBuild && satisfiesViteVersion("7.0.0"))( - "returns the prerendered route at /prerendered after the build", - async ({ expect }) => { - await page.goto(`${viteTestUrl}/prerendered`); - const content = await page.textContent("h1"); - expect(content).toBe("Pre-rendered HTML"); - } -); diff --git a/packages/vite-plugin-cloudflare/playground/prerendering/package.json b/packages/vite-plugin-cloudflare/playground/prerendering/package.json index 3ffe6c0f90..13694c2a62 100644 --- a/packages/vite-plugin-cloudflare/playground/prerendering/package.json +++ b/packages/vite-plugin-cloudflare/playground/prerendering/package.json @@ -4,6 +4,7 @@ "type": "module", "scripts": { "build:default": "vite build", + "build:assets-only": "vite build -c ./vite.config.assets-only.ts", "check:type": "tsc --build", "dev": "vite dev", "preview": "vite preview" diff --git a/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.assets-only.ts b/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.assets-only.ts new file mode 100644 index 0000000000..660e01f6ca --- /dev/null +++ b/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.assets-only.ts @@ -0,0 +1,3 @@ +import { createConfig } from "./vite.config"; + +export default createConfig(true); diff --git a/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.ts b/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.ts index 78abaebb18..4fc61a019a 100644 --- a/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.ts +++ b/packages/vite-plugin-cloudflare/playground/prerendering/vite.config.ts @@ -3,62 +3,77 @@ import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { cloudflare } from "@cloudflare/vite-plugin"; import { defineConfig, preview } from "vite"; +import { satisfiesMinimumViteVersion } from "../__test-utils__/vite-version"; -export default defineConfig({ - plugins: [ - cloudflare({ - inspectorPort: false, - persistState: false, - experimental: { - prerenderWorker: { - config(_, { entryWorkerConfig }) { - return { - ...entryWorkerConfig, - name: "prerender", - main: "./src/prerender.ts", - }; - }, - }, - }, - }), - { - name: "prerender-plugin", - enforce: "post", - buildApp: { - order: "post", - async handler(builder) { - const previewServer = await preview({ - // Needed because the tests are run from a different directory - root: path.dirname(fileURLToPath(import.meta.url)), - logLevel: "silent", - preview: { - port: 0, - }, - }); +export function createConfig(assetsOnly: boolean) { + return defineConfig( + // These playground tests don't run in Vite 6 because the feature is not compatible + // We return an empty Vite config so that the preview server can still start + !satisfiesMinimumViteVersion("7.0.0") + ? {} + : { + plugins: [ + cloudflare({ + inspectorPort: false, + persistState: false, + viteEnvironment: { name: "ssr" }, + assetsOnly: () => assetsOnly, + experimental: { + prerenderWorker: { + config(_, { entryWorkerConfig }) { + return { + ...entryWorkerConfig, + name: "prerender", + main: "./src/prerender.ts", + }; + }, + }, + }, + }), + { + name: "prerender-plugin", + enforce: "post", + buildApp: { + order: "post", + async handler(builder) { + const previewServer = await preview({ + // Needed because the tests are run from a different directory + root: path.dirname(fileURLToPath(import.meta.url)), + logLevel: "silent", + preview: { + port: 0, + }, + }); + + const baseUrl = previewServer.resolvedUrls?.local[0]; + const clientOutputDirectory = + builder.environments.client?.config.build.outDir; - const baseUrl = previewServer.resolvedUrls?.local[0]; - const clientOutputDirectory = - builder.environments.client?.config.build.outDir; + if (baseUrl && clientOutputDirectory) { + const response = await fetch( + new URL("/prerendered", baseUrl) + ); + const html = await response.text(); - if (baseUrl && clientOutputDirectory) { - const response = await fetch(new URL("/prerendered", baseUrl)); - const html = await response.text(); + await fsp.writeFile( + path.resolve( + builder.config.root, + clientOutputDirectory, + "prerendered.html" + ), + html + ); - await fsp.writeFile( - path.resolve( - builder.config.root, - clientOutputDirectory, - "prerendered.html" - ), - html - ); + console.log("Pre-rendered /prerendered"); + } - console.log("Pre-rendered /prerendered"); - } + await previewServer.close(); + }, + }, + }, + ], + } + ); +} - await previewServer.close(); - }, - }, - }, - ], -}); +export default createConfig(false); diff --git a/packages/vite-plugin-cloudflare/src/build.ts b/packages/vite-plugin-cloudflare/src/build.ts index 2b2d277eb5..bfd6fd43e8 100644 --- a/packages/vite-plugin-cloudflare/src/build.ts +++ b/packages/vite-plugin-cloudflare/src/build.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import * as fs from "node:fs"; import * as path from "node:path"; import colors from "picocolors"; +import { resolveDevOnly } from "./plugin-config"; import { VIRTUAL_CLIENT_FALLBACK_ENTRY } from "./plugins/virtual-modules"; import { satisfiesMinimumViteVersion } from "./utils"; import type { @@ -25,19 +26,28 @@ export function createBuildApp( fs.existsSync(defaultHtmlPath); const workerEnvironments = [ - ...resolvedPluginConfig.environmentNameToWorkerMap.keys(), - ].map((environmentName) => { - const environment = builder.environments[environmentName]; - assert(environment, `"${environmentName}" environment not found`); + ...resolvedPluginConfig.environmentNameToWorkerMap.entries(), + ] + .filter(([_, worker]) => !resolveDevOnly(worker.devOnly)) + .map(([environmentName]) => { + const environment = builder.environments[environmentName]; + assert(environment, `"${environmentName}" environment not found`); - return environment; - }); + return environment; + }); await Promise.all( workerEnvironments.map((environment) => builder.build(environment)) ); - if (resolvedPluginConfig.type === "assets-only") { + const isAssetsOnly = + resolvedPluginConfig.type === "assets-only" || + !workerEnvironments.some( + (environment) => + environment.name === resolvedPluginConfig.entryWorkerEnvironmentName + ); + + if (isAssetsOnly) { if (hasClientEntry) { await builder.build(clientEnvironment); } else if ( diff --git a/packages/vite-plugin-cloudflare/src/deploy-config.ts b/packages/vite-plugin-cloudflare/src/deploy-config.ts index 722617ff75..6c9b117c71 100644 --- a/packages/vite-plugin-cloudflare/src/deploy-config.ts +++ b/packages/vite-plugin-cloudflare/src/deploy-config.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import * as fs from "node:fs"; import * as path from "node:path"; import * as wrangler from "wrangler"; +import { resolveDevOnly } from "./plugin-config"; import type { AssetsOnlyResolvedConfig, WorkersResolvedConfig, @@ -27,8 +28,7 @@ export function getWorkerConfigs(root: string, isPrerender: boolean) { return [ ...(isPrerender && deployConfig.prerenderWorkerConfigPath ? [{ configPath: deployConfig.prerenderWorkerConfigPath }] - : []), - { configPath: deployConfig.configPath }, + : [{ configPath: deployConfig.configPath }]), ...deployConfig.auxiliaryWorkers, ].map(({ configPath }) => { const resolvedConfigPath = path.resolve( @@ -39,101 +39,65 @@ export function getWorkerConfigs(root: string, isPrerender: boolean) { }); } -function getRelativePathToWorkerConfig( - deployConfigDirectory: string, - root: string, - outputDirectory: string -) { - return path.relative( - deployConfigDirectory, - path.resolve(root, outputDirectory, "wrangler.json") - ); -} - export function writeDeployConfig( resolvedPluginConfig: AssetsOnlyResolvedConfig | WorkersResolvedConfig, - resolvedViteConfig: vite.ResolvedConfig + resolvedViteConfig: vite.ResolvedConfig, + isAssetsOnly: boolean ) { const deployConfigPath = getDeployConfigPath(resolvedViteConfig.root); const deployConfigDirectory = path.dirname(deployConfigPath); fs.mkdirSync(deployConfigDirectory, { recursive: true }); - if (resolvedPluginConfig.type === "assets-only") { - const clientOutputDirectory = - resolvedViteConfig.environments.client?.build.outDir; + const resolveConfigPath = (environmentName: string) => { + const outputDirectory = + resolvedViteConfig.environments[environmentName]?.build.outDir; assert( - clientOutputDirectory, - "Unexpected error: client environment output directory is undefined" + outputDirectory, + `Unexpected error: ${environmentName} environment output directory is undefined` ); - const prerenderOutputDirectory = - resolvedPluginConfig.prerenderWorkerEnvironmentName - ? resolvedViteConfig.environments[ - resolvedPluginConfig.prerenderWorkerEnvironmentName - ]?.build.outDir - : undefined; - - const deployConfig: DeployConfig = { - configPath: getRelativePathToWorkerConfig( - deployConfigDirectory, - resolvedViteConfig.root, - clientOutputDirectory - ), - auxiliaryWorkers: [], - prerenderWorkerConfigPath: prerenderOutputDirectory - ? getRelativePathToWorkerConfig( - deployConfigDirectory, - resolvedViteConfig.root, - prerenderOutputDirectory + return path.relative( + deployConfigDirectory, + path.resolve(resolvedViteConfig.root, outputDirectory, "wrangler.json") + ); + }; + + const auxiliaryWorkerEnvironmentNames = + resolvedPluginConfig.type === "workers" + ? [...resolvedPluginConfig.environmentNameToWorkerMap.entries()] + .filter( + ([name, worker]) => + name !== resolvedPluginConfig.entryWorkerEnvironmentName && + name !== resolvedPluginConfig.prerenderWorkerEnvironmentName && + !resolveDevOnly(worker.devOnly) ) - : undefined, - }; - - fs.writeFileSync(deployConfigPath, JSON.stringify(deployConfig)); - } else { - let entryWorkerConfigPath: string | undefined; - let prerenderWorkerConfigPath: string | undefined; - const auxiliaryWorkers: DeployConfig["auxiliaryWorkers"] = []; + .map(([name]) => name) + : []; - for (const environmentName of resolvedPluginConfig.environmentNameToWorkerMap.keys()) { - const outputDirectory = - resolvedViteConfig.environments[environmentName]?.build.outDir; - - assert( - outputDirectory, - `Unexpected error: ${environmentName} environment output directory is undefined` - ); - - const configPath = getRelativePathToWorkerConfig( - deployConfigDirectory, - resolvedViteConfig.root, - outputDirectory - ); - - if (environmentName === resolvedPluginConfig.entryWorkerEnvironmentName) { - entryWorkerConfigPath = configPath; - } else if ( - environmentName === resolvedPluginConfig.prerenderWorkerEnvironmentName - ) { - prerenderWorkerConfigPath = configPath; - } else { - auxiliaryWorkers.push({ configPath }); - } - } + let entryEnvironmentName: string; + if (isAssetsOnly) { + entryEnvironmentName = "client"; + } else { assert( - entryWorkerConfigPath, - `Unexpected error: entryWorkerConfigPath is undefined` + resolvedPluginConfig.type === "workers", + `Unexpected error: expected workers config but got ${resolvedPluginConfig.type}` ); + entryEnvironmentName = resolvedPluginConfig.entryWorkerEnvironmentName; + } - const deployConfig: DeployConfig = { - configPath: entryWorkerConfigPath, - auxiliaryWorkers, - prerenderWorkerConfigPath, - }; + const deployConfig: DeployConfig = { + configPath: resolveConfigPath(entryEnvironmentName), + auxiliaryWorkers: auxiliaryWorkerEnvironmentNames.map((name) => ({ + configPath: resolveConfigPath(name), + })), + prerenderWorkerConfigPath: + resolvedPluginConfig.prerenderWorkerEnvironmentName + ? resolveConfigPath(resolvedPluginConfig.prerenderWorkerEnvironmentName) + : undefined, + }; - fs.writeFileSync(deployConfigPath, JSON.stringify(deployConfig)); - } + fs.writeFileSync(deployConfigPath, JSON.stringify(deployConfig)); } diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index c575907c65..aa152fa16a 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -32,29 +32,62 @@ interface BaseWorkerConfig { viteEnvironment?: { name?: string; childEnvironments?: string[] }; } +/** + * Whether this Worker is only used during development and should not be built for production. + * Can be a boolean or a function that returns a boolean. The function is evaluated lazily + * at build time, allowing frameworks to provide the value after initialization. + */ +type DevOnly = boolean | (() => boolean); + interface EntryWorkerConfig extends BaseWorkerConfig { configPath?: string; config?: WorkerConfigCustomizer; + /** + * Whether the entry Worker should be omitted from the production build. + * Can be a boolean or a function that returns a boolean. The function is + * evaluated lazily at build time, allowing frameworks to provide the value + * after initialization. + * + * When set, an assets-only Wrangler config is emitted to the client output + * directory. This enables using server-side code in development but producing + * a fully static app for deployment. + */ + assetsOnly?: DevOnly; } interface AuxiliaryWorkerFileConfig extends BaseWorkerConfig { configPath: string; + devOnly?: DevOnly; } interface AuxiliaryWorkerInlineConfig extends BaseWorkerConfig { configPath?: string; config: WorkerConfigCustomizer; + devOnly?: DevOnly; } type AuxiliaryWorkerConfig = | AuxiliaryWorkerFileConfig | AuxiliaryWorkerInlineConfig; +interface PrerenderWorkerFileConfig extends BaseWorkerConfig { + configPath: string; +} + +interface PrerenderWorkerInlineConfig extends BaseWorkerConfig { + configPath?: string; + config: WorkerConfigCustomizer; +} + +type PrerenderWorkerConfig = + | PrerenderWorkerFileConfig + | PrerenderWorkerInlineConfig; + interface Experimental { /** Experimental support for handling the _headers and _redirects files during Vite dev mode. */ headersAndRedirectsDevModeSupport?: boolean; /** Experimental support for a dedicated prerender Worker */ - prerenderWorker?: AuxiliaryWorkerConfig; + prerenderWorker?: PrerenderWorkerConfig; } type FilteredEntryWorkerConfig = Omit< @@ -95,6 +128,7 @@ export interface ResolvedWorkerConfig extends ResolvedAssetsOnlyConfig { export interface Worker { config: ResolvedWorkerConfig; nodeJsCompat: NodeJsCompat | undefined; + devOnly: DevOnly | undefined; } interface BaseResolvedConfig { @@ -360,7 +394,10 @@ export function resolvePluginConfig( environmentNameToWorkerMap.set( prerenderWorkerEnvironmentName, - resolveWorker(workerResolvedConfig.config as ResolvedWorkerConfig) + resolveWorker( + workerResolvedConfig.config as ResolvedWorkerConfig, + undefined + ) ); const prerenderWorkerChildEnvironments = @@ -413,7 +450,7 @@ export function resolvePluginConfig( environmentNameToWorkerMap.set( entryWorkerEnvironmentName, - resolveWorker(entryWorkerResolvedConfig.config) + resolveWorker(entryWorkerResolvedConfig.config, pluginConfig.assetsOnly) ); const entryWorkerChildEnvironments = @@ -464,7 +501,10 @@ export function resolvePluginConfig( environmentNameToWorkerMap.set( workerEnvironmentName, - resolveWorker(workerResolvedConfig.config as ResolvedWorkerConfig) + resolveWorker( + workerResolvedConfig.config as ResolvedWorkerConfig, + auxiliaryWorker.devOnly + ) ); const auxiliaryWorkerChildEnvironments = @@ -521,11 +561,27 @@ function createEnvironmentNameValidator() { }; } -function resolveWorker(workerConfig: ResolvedWorkerConfig): Worker { +/** + * Evaluates the `devOnly` value. Should be called lazily at build time + * to allow frameworks to provide the value after initialization. + */ +export function resolveDevOnly(devOnly: DevOnly | undefined): boolean { + if (typeof devOnly === "function") { + return devOnly(); + } + + return devOnly ?? false; +} + +function resolveWorker( + workerConfig: ResolvedWorkerConfig, + devOnly: DevOnly | undefined +): Worker { return { config: workerConfig, nodeJsCompat: hasNodeJsCompat(workerConfig) ? new NodeJsCompat(workerConfig) : undefined, + devOnly, }; } diff --git a/packages/vite-plugin-cloudflare/src/plugins/config.ts b/packages/vite-plugin-cloudflare/src/plugins/config.ts index 0b5ec6b804..104fd745ab 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/config.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/config.ts @@ -1,4 +1,5 @@ import assert from "node:assert"; +import * as fs from "node:fs"; import * as path from "node:path"; import { normalizePath } from "vite"; import { hasAssetsConfigChanged } from "../asset-config"; @@ -8,12 +9,15 @@ import { createCloudflareEnvironmentOptions, } from "../cloudflare-environment"; import { assertIsNotPreview } from "../context"; +import { writeDeployConfig } from "../deploy-config"; import { hasLocalDevVarsFileChanged } from "../dev-vars"; +import { resolveDevOnly } from "../plugin-config"; import { createPlugin, debuglog, getOutputDirectory } from "../utils"; import { validateWorkerEnvironmentOptions } from "../vite-config"; import { getWarningForWorkersConfigs } from "../workers-configs"; import type { PluginContext } from "../context"; import type { EnvironmentOptions, UserConfig } from "vite"; +import type { Unstable_RawConfig } from "wrangler"; /** * Plugin to handle configuration and config file watching @@ -122,15 +126,17 @@ export const configPlugin = createPlugin("config", (ctx) => { } const workerEnvironments = [ - ...ctx.resolvedPluginConfig.environmentNameToWorkerMap.keys(), - ].map((environmentName) => { - const environment = builder.environments[environmentName]; - assert(environment, `"${environmentName}" environment not found`); + ...ctx.resolvedPluginConfig.environmentNameToWorkerMap.entries(), + ] + .filter(([_, worker]) => !resolveDevOnly(worker.devOnly)) + .map(([environmentName]) => { + const environment = builder.environments[environmentName]; + assert(environment, `"${environmentName}" environment not found`); - return environment; - }); + return environment; + }); - // Build any Worker environments that haven't already been built + // Build Worker environments that have not yet been built and are not dev-only await Promise.all( workerEnvironments .filter((environment) => !environment.isBuilt) @@ -144,13 +150,69 @@ export const configPlugin = createPlugin("config", (ctx) => { entryWorkerEnvironment, `No "${entryWorkerEnvironmentName}" environment` ); - const entryWorkerBuildDirectory = path.resolve( - builder.config.root, - entryWorkerEnvironment.config.build.outDir - ); - if (!builder.environments.client?.isBuilt) { - removeAssetsField(entryWorkerBuildDirectory); + if (entryWorkerEnvironment.isBuilt) { + if (!builder.environments.client?.isBuilt) { + // The client environment was not built so we remove the assets config + + const entryWorkerBuildDirectory = path.resolve( + builder.config.root, + entryWorkerEnvironment.config.build.outDir + ); + + removeAssetsField(entryWorkerBuildDirectory); + } + } else { + // The entry Worker was only used in development so we emit an assets-only config to the client build output + + const clientEnvironment = builder.environments.client; + assert(clientEnvironment, 'No "client" environment'); + + if (!clientEnvironment.isBuilt) { + throw new Error( + "If `assetsOnly` is set to `true`, the client environment must be built" + ); + } + + const entryWorkerConfig = ctx.getWorkerConfig( + entryWorkerEnvironmentName + ); + assert( + entryWorkerConfig, + `No config found for "${entryWorkerEnvironmentName}" environment` + ); + + const outputConfig: Unstable_RawConfig = { + ...entryWorkerConfig, + main: undefined, + assets: { + ...entryWorkerConfig.assets, + directory: ".", + binding: undefined, + }, + }; + + if ( + outputConfig.unsafe && + Object.keys(outputConfig.unsafe).length === 0 + ) { + outputConfig.unsafe = undefined; + } + + fs.writeFileSync( + path.resolve( + builder.config.root, + clientEnvironment.config.build.outDir, + "wrangler.json" + ), + JSON.stringify(outputConfig) + ); + + writeDeployConfig( + ctx.resolvedPluginConfig, + ctx.resolvedViteConfig, + true + ); } }, }, diff --git a/packages/vite-plugin-cloudflare/src/plugins/output-config.ts b/packages/vite-plugin-cloudflare/src/plugins/output-config.ts index add40e4a77..0d046b62d4 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/output-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/output-config.ts @@ -1,5 +1,5 @@ import assert from "node:assert"; -import { existsSync, readFileSync } from "node:fs"; +import * as fs from "node:fs"; import * as path from "node:path"; import * as vite from "vite"; import { MAIN_ENTRY_NAME } from "../cloudflare-environment"; @@ -137,22 +137,25 @@ export const outputConfigPlugin = createPlugin("output-config", (ctx) => { assertIsNotPreview(ctx); // These conditions ensure the deploy config is emitted once per application build as `writeBundle` is called for each environment. - // If Vite introduces an additional hook that runs after the application has built then we could use that instead. if ( this.environment.name === (ctx.resolvedPluginConfig.type === "workers" ? ctx.resolvedPluginConfig.entryWorkerEnvironmentName : "client") ) { - writeDeployConfig(ctx.resolvedPluginConfig, ctx.resolvedViteConfig); + writeDeployConfig( + ctx.resolvedPluginConfig, + ctx.resolvedViteConfig, + ctx.resolvedPluginConfig.type === "assets-only" + ); } }, }; }); function readAssetsIgnoreFile(assetsIgnorePath: string): string { - const content = existsSync(assetsIgnorePath) - ? readFileSync(assetsIgnorePath, "utf-8") + const content = fs.existsSync(assetsIgnorePath) + ? fs.readFileSync(assetsIgnorePath, "utf-8") : ""; if (content.length === 0) { return ""; From ca5b604639eabbcb7385537801d1fdd72cf93144 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 26 May 2026 14:50:54 +0100 Subject: [PATCH 6/7] Add telemetry for detecting whether AI coding agents have Cloudflare skills installed (#14009) --- .changeset/agent-skills-telemetry.md | 7 + .../__tests__/agents-skills-install.test.ts | 724 +++++++++++++++++- .../wrangler/src/__tests__/metrics.test.ts | 10 +- .../wrangler/src/__tests__/vitest.setup.ts | 15 +- .../wrangler/src/agents-skills-install.ts | 506 +++++++++++- .../src/metrics/metrics-dispatcher.ts | 85 +- packages/wrangler/telemetry.md | 1 + 7 files changed, 1300 insertions(+), 48 deletions(-) create mode 100644 .changeset/agent-skills-telemetry.md diff --git a/.changeset/agent-skills-telemetry.md b/.changeset/agent-skills-telemetry.md new file mode 100644 index 0000000000..3d610daf33 --- /dev/null +++ b/.changeset/agent-skills-telemetry.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Add telemetry for detecting whether AI coding agents have Cloudflare skills installed + +Wrangler now includes a `currentAgentSkillsInstalled` property in telemetry events that reports whether the current AI coding agent has Cloudflare skills present on disk. The value distinguishes between skills installed automatically by Wrangler (`"automatic"`), skills installed manually by the user (`"manual"`), no skills present (`false`), or no supported agent detected (`null`). Skill names are fetched from the GitHub Contents API with a 24-hour disk cache to avoid rate limits. diff --git a/packages/wrangler/src/__tests__/agents-skills-install.test.ts b/packages/wrangler/src/__tests__/agents-skills-install.test.ts index ec9151f6b7..de1fa34f90 100644 --- a/packages/wrangler/src/__tests__/agents-skills-install.test.ts +++ b/packages/wrangler/src/__tests__/agents-skills-install.test.ts @@ -1,19 +1,28 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; import { getGlobalWranglerConfigPath } from "@cloudflare/workers-utils"; import { runInTempDir } from "@cloudflare/workers-utils/test-helpers"; +import { detectAgenticEnvironment } from "am-i-vibing"; import ci from "ci-info"; +import { http, HttpResponse } from "msw"; import { afterEach, beforeEach, describe, test, vi } from "vitest"; import { sendMetricsEvent } from "../metrics/send-event"; import { mockConsoleMethods } from "./helpers/mock-console"; import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs"; import { useMockIsTTY } from "./helpers/mock-istty"; -import type { maybeInstallCloudflareSkillsGlobally as InstallFnType } from "../agents-skills-install"; +import { msw } from "./helpers/msw"; +import type { + maybeInstallCloudflareSkillsGlobally as InstallFnType, + telemetryCurrentAgentSkillsInstalled as TelemetryFnType, +} from "../agents-skills-install"; import type * as SendEventModule from "../metrics/send-event"; // Undo the global no-op mock from vitest.setup.ts so we test the real implementation 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(); @@ -92,6 +101,30 @@ async function freshImport(): Promise { return mod.maybeInstallCloudflareSkillsGlobally; } +/** + * Like {@link freshImport} but returns the telemetry function instead. + * Each call resets the module graph (and hence the memoised promise) so + * that tests start with a clean state. + * + * @returns The `telemetryCurrentAgentSkillsInstalled` function from a fresh module import. + */ +async function freshTelemetryImport(): Promise { + vi.resetModules(); + const mod = await import("../agents-skills-install"); + return mod.telemetryCurrentAgentSkillsInstalled; +} + +/** + * Creates an agent's global config directory under `os.homedir()`. + * This is needed because the telemetry function checks for skills in + * agent-specific directories. + * + * @param dirName The agent's directory name relative to $HOME (e.g. ".claude"). + */ +function createAgentDir(dirName: string): void { + mkdirSync(path.join(os.homedir(), dirName), { recursive: true }); +} + describe("maybeInstallCloudflareSkillsGlobally", () => { runInTempDir(); const std = mockConsoleMethods(); @@ -121,6 +154,41 @@ describe("maybeInstallCloudflareSkillsGlobally", () => { expect(sendMetricsEvent).not.toHaveBeenCalled(); }); + // TODO(dario): Remove this migration branch after 2026-06-05 — by then + // most active users' metadata files will have been converted to version 1. + test("skips silently when metadata file uses the legacy format (no version field) and migrates it on disk", async ({ + expect, + }) => { + writeMetadataFile({ + accepted: true, + date: "2025-07-01T00:00:00Z", + detectedAgents: [ + { + name: "Claude Code", + rosieId: "claude", + globalSkillsPath: "/fake/.claude/skills", + }, + ], + installFailed: false, + }); + const maybeInstallCloudflareSkillsGlobally = await freshImport(); + + await maybeInstallCloudflareSkillsGlobally(false); + + expect(mockRosieAgents).not.toHaveBeenCalled(); + expect(mockRosieInstall).not.toHaveBeenCalled(); + expect(sendMetricsEvent).not.toHaveBeenCalled(); + // Verify the file was migrated to the new format on disk + const migratedMetadata = readMetadataFile(); + expect(migratedMetadata.version).toBe(1); + expect(migratedMetadata.detectedAgents).toEqual([ + { + name: "Claude Code", + rosie: { id: "claude", globalPath: "/fake/.claude/skills" }, + }, + ]); + }); + test("force=true ignores existing metadata file", async ({ expect }) => { writeMetadataFile({ accepted: true, date: "2025-01-01T00:00:00Z" }); const maybeInstallCloudflareSkillsGlobally = await freshImport(); @@ -329,8 +397,10 @@ describe("maybeInstallCloudflareSkillsGlobally", () => { agents: [ { name: "Claude Code", - rosieId: "claude", - globalSkillsPath: "/fake/.claude/skills", + rosie: { + id: "claude", + globalPath: "/fake/.claude/skills", + }, }, ], }, @@ -484,7 +554,7 @@ describe("maybeInstallCloudflareSkillsGlobally", () => { expect.arrayContaining([ expect.objectContaining({ name: "Claude Code", - rosieId: "claude", + rosie: expect.objectContaining({ id: "claude" }), }), ]) ); @@ -569,3 +639,649 @@ describe("maybeInstallCloudflareSkillsGlobally", () => { }); }); }); + +/** + * MSW handler that returns a fake GitHub Contents API response. + * + * @param skillNames The skill directory names to include in the mocked response. + */ +function mockGitHubSkillsApi(skillNames: string[]) { + const entries = skillNames.map((name) => ({ name, type: "dir" })); + msw.use( + http.get( + "https://api.github.com/repos/cloudflare/skills/contents/skills", + () => { + return HttpResponse.json(entries); + } + ) + ); +} + +/** + * MSW handler that makes the GitHub Contents API return an error. + * + * @param status The HTTP status code to return. Defaults to `403`. + */ +function mockGitHubSkillsApiError(status = 403) { + msw.use( + http.get( + "https://api.github.com/repos/cloudflare/skills/contents/skills", + () => { + return new HttpResponse(null, { status }); + } + ) + ); +} + +/** MSW handler that makes the GitHub Contents API throw a network error. */ +function mockGitHubSkillsApiNetworkError() { + msw.use( + http.get( + "https://api.github.com/repos/cloudflare/skills/contents/skills", + () => { + return HttpResponse.error(); + } + ) + ); +} + +describe("telemetryCurrentAgentSkillsInstalled", () => { + runInTempDir(); + mockConsoleMethods(); + + beforeEach(() => { + // Default: no agent detected + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: false, + id: null, + name: null, + type: null, + }); + }); + + test("resolves to null when no agent is detected", async ({ expect }) => { + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe(null); + }); + + test("resolves to null when detectAgenticEnvironment throws", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockImplementation(() => { + throw new Error("Detection failed"); + }); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe(null); + }); + + test("resolves to null when agent is detected but not in telemetryAgentMappings", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "jules", + name: "Jules", + type: "agent", + }); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe(null); + }); + + test("resolves to false when GitHub API fetch fails and no cache exists", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + mockGitHubSkillsApiNetworkError(); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe(false); + }); + + test("resolves to false when no skills are present in agent's globalSkillsPath", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe(false); + }); + + test("resolves to 'manual' when some skills exist but no metadata file", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + + test("resolves to 'automatic' when skills exist and metadata confirms successful install", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + const claudeGlobalSkillsPath = path.join(os.homedir(), ".claude", "skills"); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Claude Code", + rosie: { id: "claude", globalPath: claudeGlobalSkillsPath }, + }, + ], + installFailed: false, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("automatic"); + }); + + test("resolves to 'manual' when metadata says install failed entirely (installFailed: true)", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + const claudeGlobalSkillsPath = path.join(os.homedir(), ".claude", "skills"); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Claude Code", + rosie: { id: "claude", globalPath: claudeGlobalSkillsPath }, + }, + ], + installFailed: true, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + + test("resolves to 'manual' when metadata says install failed for this agent (installFailed: string[])", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + const claudeGlobalSkillsPath = path.join(os.homedir(), ".claude", "skills"); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Claude Code", + rosie: { id: "claude", globalPath: claudeGlobalSkillsPath }, + }, + ], + installFailed: ["claude"], + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + + test("resolves to 'manual' when agent is not in detectedAgents", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [], + installFailed: false, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + + test("uses cached GitHub API response within TTL", async ({ expect }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + + // Write a fresh cache file + const configDir = getGlobalWranglerConfigPath(); + mkdirSync(configDir, { recursive: true }); + writeFileSync( + path.join(configDir, "cloudflare-skills-repo-cache.json"), + JSON.stringify({ + lastUpdate: Date.now(), + skillNames: ["cloudflare", "wrangler"], + }) + ); + + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Claude Code", + rosie: { + id: "claude", + globalPath: path.join(os.homedir(), ".claude", "skills"), + }, + }, + ], + installFailed: false, + }); + + // Do NOT set up a GitHub API mock — if fetch is called, it would + // go unhandled. The test verifies the cache is used instead. + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("automatic"); + }); + + test("falls back to stale cache when GitHub API returns an error", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + + // Write an expired cache file (TTL is 24h, set lastUpdate to 48h ago) + const configDir = getGlobalWranglerConfigPath(); + mkdirSync(configDir, { recursive: true }); + writeFileSync( + path.join(configDir, "cloudflare-skills-repo-cache.json"), + JSON.stringify({ + lastUpdate: Date.now() - 48 * 60 * 60 * 1000, + skillNames: ["cloudflare", "wrangler"], + }) + ); + + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Claude Code", + rosie: { + id: "claude", + globalPath: path.join(os.homedir(), ".claude", "skills"), + }, + }, + ], + installFailed: false, + }); + + mockGitHubSkillsApiError(403); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("automatic"); + }); + + test("works with cursor-agent amIVibingId mapping", async ({ expect }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "cursor-agent", + name: "Cursor Agent", + type: "agent", + }); + createAgentDir(".cursor"); + const cursorSkills = path.join(os.homedir(), ".cursor", "skills"); + mkdirSync(path.join(cursorSkills, "cloudflare"), { recursive: true }); + const cursorGlobalSkillsPath = path.join(os.homedir(), ".cursor", "skills"); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Cursor", + rosie: { id: "cursor", globalPath: cursorGlobalSkillsPath }, + }, + ], + installFailed: false, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("automatic"); + }); + + test("resolves to 'manual' when skills exist at an alternativeGlobalPath but no metadata", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "opencode", + name: "OpenCode", + type: "agent", + }); + // Primary rosie path (~/.config/opencode/skills) is empty, but skills + // exist in the alternative path (~/.agents/skills). + createAgentDir(".config/opencode"); + const agentsSkills = path.join(os.homedir(), ".agents", "skills"); + mkdirSync(path.join(agentsSkills, "cloudflare"), { recursive: true }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + + test("resolves to 'automatic' when skills at alternativeGlobalPath were installed for another agent", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "opencode", + name: "OpenCode", + type: "agent", + }); + // Primary rosie path (~/.config/opencode/skills) is empty, but skills + // exist in ~/.agents/skills (which is Warp's rosie install target). + createAgentDir(".config/opencode"); + const agentsSkills = path.join(os.homedir(), ".agents", "skills"); + mkdirSync(path.join(agentsSkills, "cloudflare"), { recursive: true }); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Cline, Dexto, Warp", + rosie: { id: "warp", globalPath: agentsSkills }, + }, + ], + installFailed: false, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("automatic"); + }); + + test("resolves to 'manual' when skills at alternativeGlobalPath were installed for another agent but failed", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "opencode", + name: "OpenCode", + type: "agent", + }); + createAgentDir(".config/opencode"); + const agentsSkills = path.join(os.homedir(), ".agents", "skills"); + mkdirSync(path.join(agentsSkills, "cloudflare"), { recursive: true }); + writeMetadataFile({ + version: 1, + accepted: true, + date: new Date().toISOString(), + detectedAgents: [ + { + name: "Cline, Dexto, Warp", + rosie: { id: "warp", globalPath: agentsSkills }, + }, + ], + installFailed: ["warp"], + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + + test("resolves to false when skills are not at primary or any alternativeGlobalPath", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "opencode", + name: "OpenCode", + type: "agent", + }); + // Create the primary path dir but leave it empty, and don't create + // any alternative paths either. + createAgentDir(".config/opencode/skills"); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe(false); + }); + + // TODO(dario): Remove this migration branch after 2026-06-05 — by then + // most active users' metadata files will have been converted to version 1. + describe("legacy metadata migration", () => { + test("resolves to 'automatic' when metadata uses the legacy flat AgentInfo schema", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + // Write old-format metadata (no version field, flat rosieId/globalSkillsPath) + writeMetadataFile({ + accepted: true, + date: "2025-07-01T00:00:00Z", + detectedAgents: [ + { + name: "Claude Code", + rosieId: "claude", + globalSkillsPath: claudeSkills, + }, + ], + installFailed: false, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("automatic"); + }); + + test("migrates legacy metadata to version 1 on disk when read", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + // Write old-format metadata + writeMetadataFile({ + accepted: true, + date: "2025-07-01T00:00:00Z", + detectedAgents: [ + { + name: "Claude Code", + rosieId: "claude", + globalSkillsPath: claudeSkills, + }, + ], + installFailed: false, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + // Trigger the read (and migration) + await telemetryCurrentAgentSkillsInstalled(); + + // The file on disk should now be in the new format + const migratedMetadata = readMetadataFile(); + expect(migratedMetadata).toEqual({ + version: 1, + accepted: true, + date: "2025-07-01T00:00:00Z", + detectedAgents: [ + { + name: "Claude Code", + rosie: { id: "claude", globalPath: claudeSkills }, + }, + ], + installFailed: false, + }); + }); + + test("resolves to 'manual' when legacy metadata says install failed", async ({ + expect, + }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: true, + id: "claude-code", + name: "Claude Code", + type: "agent", + }); + createAgentDir(".claude"); + const claudeSkills = path.join(os.homedir(), ".claude", "skills"); + mkdirSync(path.join(claudeSkills, "cloudflare"), { recursive: true }); + // Write old-format metadata with installFailed: true + writeMetadataFile({ + accepted: true, + date: "2025-07-01T00:00:00Z", + detectedAgents: [ + { + name: "Claude Code", + rosieId: "claude", + globalSkillsPath: claudeSkills, + }, + ], + installFailed: true, + }); + mockGitHubSkillsApi(["cloudflare", "wrangler"]); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const result = await telemetryCurrentAgentSkillsInstalled(); + + expect(result).toBe("manual"); + }); + }); + + test("memoises the result across multiple calls", async ({ expect }) => { + vi.mocked(detectAgenticEnvironment).mockReturnValue({ + isAgentic: false, + id: null, + name: null, + type: null, + }); + const telemetryCurrentAgentSkillsInstalled = await freshTelemetryImport(); + + const first = telemetryCurrentAgentSkillsInstalled(); + const second = telemetryCurrentAgentSkillsInstalled(); + + expect(first).toBe(second); + expect(await first).toBe(null); + }); +}); diff --git a/packages/wrangler/src/__tests__/metrics.test.ts b/packages/wrangler/src/__tests__/metrics.test.ts index c7ff11c8b1..2bab74e4e8 100644 --- a/packages/wrangler/src/__tests__/metrics.test.ts +++ b/packages/wrangler/src/__tests__/metrics.test.ts @@ -108,7 +108,7 @@ describe("metrics", () => { await allMetricsDispatchesCompleted(); expect(requests.count).toBe(1); expect(std.debug).toMatchInlineSnapshot( - `"Metrics dispatcher: Posting data {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2}}"` + `"Metrics dispatcher: Posting data {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2,"currentAgentSkillsInstalled":null}}"` ); expect(std.out).toMatchInlineSnapshot(`""`); expect(std.warn).toMatchInlineSnapshot(`""`); @@ -140,10 +140,11 @@ describe("metrics", () => { sendMetrics: false, }); dispatcher.sendAdhocEvent("some-event", { a: 1, b: 2 }); + await allMetricsDispatchesCompleted(); expect(requests.count).toBe(0); expect(std.debug).toMatchInlineSnapshot( - `"Metrics dispatcher: Dispatching disabled - would have sent {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2}}."` + `"Metrics dispatcher: Dispatching disabled - would have sent {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2,"currentAgentSkillsInstalled":null}}."` ); expect(std.out).toMatchInlineSnapshot(`""`); expect(std.warn).toMatchInlineSnapshot(`""`); @@ -165,7 +166,7 @@ describe("metrics", () => { await allMetricsDispatchesCompleted(); expect(std.debug).toMatchInlineSnapshot(` - "Metrics dispatcher: Posting data {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2}} + "Metrics dispatcher: Posting data {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2,"currentAgentSkillsInstalled":null}} Metrics dispatcher: Failed to send request: Failed to fetch" `); expect(std.out).toMatchInlineSnapshot(`""`); @@ -183,10 +184,11 @@ describe("metrics", () => { sendMetrics: true, }); dispatcher.sendAdhocEvent("some-event", { a: 1, b: 2 }); + await allMetricsDispatchesCompleted(); expect(requests.count).toBe(0); expect(std.debug).toMatchInlineSnapshot( - `"Metrics dispatcher: Source Key not provided. Be sure to initialize before sending events {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2}}"` + `"Metrics dispatcher: Source Key not provided. Be sure to initialize before sending events {"deviceId":"f82b1f46-eb7b-4154-aa9f-ce95f23b2288","event":"some-event","timestamp":1733961600000,"properties":{"amplitude_session_id":1733961600000,"amplitude_event_id":0,"wranglerVersion":"1.2.3","wranglerMajorVersion":1,"wranglerMinorVersion":2,"wranglerPatchVersion":3,"osPlatform":"mock platform","osVersion":"mock os version","nodeVersion":1,"packageManager":"npm","isFirstUsage":false,"configFileType":"none","isCI":false,"isPagesCI":false,"isWorkersCI":false,"isInteractive":true,"hasAssets":false,"agent":null,"category":"Workers","os":"foo:bar","a":1,"b":2,"currentAgentSkillsInstalled":null}}"` ); expect(std.out).toMatchInlineSnapshot(`""`); expect(std.warn).toMatchInlineSnapshot(`""`); diff --git a/packages/wrangler/src/__tests__/vitest.setup.ts b/packages/wrangler/src/__tests__/vitest.setup.ts index 8cbab7e541..9986dfcb2d 100644 --- a/packages/wrangler/src/__tests__/vitest.setup.ts +++ b/packages/wrangler/src/__tests__/vitest.setup.ts @@ -221,9 +221,18 @@ vi.mock("../metrics/metrics-config", async (importOriginal) => { return realModule; }); -vi.mock("../agents-skills-install", () => ({ - maybeInstallCloudflareSkillsGlobally: vi.fn().mockResolvedValue(undefined), -})); +vi.mock("../agents-skills-install", async (importOriginal) => { + const realModule = + await importOriginal(); + vi.spyOn( + realModule, + "maybeInstallCloudflareSkillsGlobally" + ).mockResolvedValue(undefined); + vi.spyOn(realModule, "telemetryCurrentAgentSkillsInstalled").mockReturnValue( + Promise.resolve(null) + ); + return realModule; +}); vi.mock("prompts", () => { return { diff --git a/packages/wrangler/src/agents-skills-install.ts b/packages/wrangler/src/agents-skills-install.ts index b2c5d92038..0e0e58bed3 100644 --- a/packages/wrangler/src/agents-skills-install.ts +++ b/packages/wrangler/src/agents-skills-install.ts @@ -1,10 +1,13 @@ -import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; import { getGlobalWranglerConfigPath, parseJSONC, } from "@cloudflare/workers-utils"; +import { detectAgenticEnvironment } from "am-i-vibing"; import ci from "ci-info"; +import { fetch } from "undici"; import { confirm } from "./dialogs"; import isInteractive from "./is-interactive"; import { logger } from "./logger"; @@ -103,6 +106,7 @@ export async function maybeInstallCloudflareSkillsGlobally( if (!accepted) { writeSkillsInstallMetadataFile({ + version: 1, accepted: false, date: new Date().toISOString(), detectedAgents, @@ -113,7 +117,7 @@ export async function maybeInstallCloudflareSkillsGlobally( try { const rosie = await import("rosie-skills"); - const agentNames = detectedAgents.map((a) => a.rosieId); + const agentNames = detectedAgents.map((a) => a.rosie.id); const { failedAgents } = await rosie.install(SKILLS_REPO, { global: true, agent: agentNames, @@ -122,7 +126,7 @@ export async function maybeInstallCloudflareSkillsGlobally( const failedSet = new Set(failedAgents); const succeededAgents = detectedAgents.filter( - (a) => !failedSet.has(a.rosieId) + (a) => !failedSet.has(a.rosie.id) ); if (succeededAgents.length > 0) { @@ -139,6 +143,7 @@ export async function maybeInstallCloudflareSkillsGlobally( } writeSkillsInstallMetadataFile({ + version: 1, accepted: true, date: new Date().toISOString(), detectedAgents, @@ -156,6 +161,7 @@ export async function maybeInstallCloudflareSkillsGlobally( logger.warn(SKILLS_INSTALL_RETRY_HINT); writeSkillsInstallMetadataFile({ + version: 1, accepted: true, date: new Date().toISOString(), detectedAgents, @@ -181,10 +187,13 @@ const SKILLS_INSTALL_RETRY_HINT = type AgentInfo = { /** Human-readable display name of the agent (e.g. "Claude Code"). */ name: string; - /** Rosie's short identifier for the agent (e.g. "claude"). */ - rosieId: string; - /** Absolute path to the agent's global skills directory. */ - globalSkillsPath: string; + /** Rosie agent metadata. */ + rosie: { + /** Rosie's short identifier for the agent (e.g. "claude"). */ + id: string; + /** Absolute path to the agent's global skills directory. */ + globalPath: string; + }; }; /** @@ -192,6 +201,8 @@ type AgentInfo = { * about installing Cloudflare agent skills globally. */ interface SkillsInstallMetadata { + /** Schema version for forward-compatibility. Currently always `1`. */ + version: 1; /** Whether the user accepted the prompt to install skills. */ accepted: boolean; /** ISO date string of when the user was prompted. */ @@ -210,6 +221,16 @@ interface SkillsInstallMetadata { /** Jsonc metadata file created when Cloudflare agent skills are installed */ const SKILLS_INSTALL_METADATA_FILENAME = "agents-skills-install.jsonc"; +/** GitHub Contents API URL for listing skill directories in the cloudflare/skills repo. */ +const SKILLS_REPO_CONTENTS_URL = + "https://api.github.com/repos/cloudflare/skills/contents/skills"; + +/** Cache filename for skill directory names fetched from the GitHub API. */ +const SKILLS_REPO_CACHE_FILENAME = "cloudflare-skills-repo-cache.json"; + +/** Time-to-live for the cached skill names (24 hours in milliseconds). */ +const SKILLS_REPO_CACHE_TTL_MS = 24 * 60 * 60 * 1000; + /** * Returns the absolute path to the skills install config file within the global wrangler config directory. */ @@ -220,15 +241,90 @@ function getSkillsInstallMetadataFilePath(): string { ); } +/** + * Returns the absolute path to the skills repo cache file within the global wrangler config directory. + */ +function getSkillsRepoCachePath(): string { + return path.resolve( + getGlobalWranglerConfigPath(), + SKILLS_REPO_CACHE_FILENAME + ); +} + +/** + * Legacy shape of `AgentInfo` written by Wrangler versions before the + * `version: 1` schema was introduced. The agent's rosie ID and global + * skills path were stored as flat properties instead of being nested + * under a `rosie` object. + */ +interface LegacyAgentInfo { + name: string; + rosieId: string; + globalSkillsPath: string; +} + +/** + * Legacy shape of the skills-install metadata file written by Wrangler + * versions before the `version: 1` schema was introduced. It lacks the + * `version` field and uses {@link LegacyAgentInfo} instead of {@link AgentInfo}. + */ +interface LegacySkillsInstallMetadata { + accepted: boolean; + date: string; + detectedAgents?: LegacyAgentInfo[]; + installFailed?: boolean | string[]; +} + +// TODO(dario): Remove `migrateMetadataToV1` and the legacy types above after +// 2026-06-05 — by then most active users' metadata files will have been +// converted to the version-1 schema. +/** + * Converts a legacy (pre-version-1) metadata object to the current schema. + * The old format stored `rosieId` / `globalSkillsPath` as flat properties on + * each agent entry; the new format nests them under `rosie: { id, globalPath }`. + */ +function migrateMetadataToV1( + old: LegacySkillsInstallMetadata +): SkillsInstallMetadata { + return { + version: 1, + accepted: old.accepted, + date: old.date, + detectedAgents: old.detectedAgents?.map((agent) => ({ + name: agent.name, + rosie: { + id: agent.rosieId, + globalPath: agent.globalSkillsPath, + }, + })), + installFailed: old.installFailed, + }; +} + /** * Reads and parses the skills install metadata file. * + * If the file uses the legacy schema (no `version` field), it is automatically + * migrated to the current version-1 format and overwritten on disk. + * * @returns The parsed metadata file, or `undefined` if the file doesn't exist or can't be parsed. */ function readSkillsInstallMetadataFile(): SkillsInstallMetadata | undefined { try { const content = readFileSync(getSkillsInstallMetadataFilePath(), "utf8"); - return parseJSONC(content) as SkillsInstallMetadata; + const parsed = parseJSONC(content) as Record; + + // TODO(dario): Remove this migration branch after 2026-06-05 — by then + // most active users' metadata files will have been converted to version 1. + if ((parsed as { version?: unknown }).version === undefined) { + const migrated = migrateMetadataToV1( + parsed as unknown as LegacySkillsInstallMetadata + ); + writeSkillsInstallMetadataFile(migrated); + return migrated; + } + + return parsed as unknown as SkillsInstallMetadata; } catch { return undefined; } @@ -243,6 +339,394 @@ function writeSkillsInstallMetadataFile(metadata: SkillsInstallMetadata): void { writeFileSync(configPath, JSON.stringify(metadata, null, "\t")); } +/** On-disk cache for skill directory names fetched from the GitHub Contents API. */ +interface SkillsRepoCache { + /** Unix-epoch millisecond timestamp of when the cache was last written. */ + lastUpdate: number; + /** Skill directory names from the `cloudflare/skills` repository. */ + skillNames: string[]; +} + +/** + * Reads the cached skill names from disk, returning them only if the cache has not expired. + * Falls back to stale data when {@link allowStale} is true (e.g. after a failed network request). + * + * @param allowStale When `true`, returns cached data even if the TTL has expired. + * @returns Array of cached skill names, or `undefined` on cache miss. + */ +function readSkillsRepoCache(allowStale = false): string[] | undefined { + try { + const raw = readFileSync(getSkillsRepoCachePath(), "utf8"); + const cache = JSON.parse(raw) as SkillsRepoCache; + if ( + allowStale || + cache.lastUpdate + SKILLS_REPO_CACHE_TTL_MS > Date.now() + ) { + return cache.skillNames; + } + } catch { + // Cache file missing, corrupt, or unreadable — treat as cache miss. + } + return undefined; +} + +/** + * Writes skill names to the disk cache with the current timestamp. + * + * @param skillNames The skill directory names to persist. + */ +function writeSkillsRepoCache(skillNames: string[]): void { + try { + const cachePath = getSkillsRepoCachePath(); + mkdirSync(path.dirname(cachePath), { recursive: true }); + const data: SkillsRepoCache = { + lastUpdate: Date.now(), + skillNames, + }; + writeFileSync(cachePath, JSON.stringify(data)); + } catch { + // Best-effort — cache write failure is non-fatal. + } +} + +/** + * Fetches the list of skill directory names from the `cloudflare/skills` GitHub + * repository using the GitHub Contents API. Results are cached to disk for 24 hours + * to avoid hitting API rate limits. + * + * @returns Array of skill directory names, or `undefined` if both fetch and cache fail. + */ +async function fetchSkillNamesFromGitHub(): Promise { + // Return fresh cached data if available. + const cached = readSkillsRepoCache(); + if (cached) { + return cached; + } + + try { + const res = await fetch(SKILLS_REPO_CONTENTS_URL, { + headers: { + Accept: "application/vnd.github.v3+json", + "User-Agent": "cloudflare-wrangler", + }, + }); + if (!res.ok) { + // API error (rate limited, 404, etc.) — fall back to stale cache. + return readSkillsRepoCache(true); + } + const entries = (await res.json()) as Array<{ + name: string; + type: string; + }>; + const skillNames = entries + .filter((e) => e.type === "dir") + .map((e) => e.name); + + writeSkillsRepoCache(skillNames); + return skillNames; + } catch { + // Network failure — fall back to stale cache. + return readSkillsRepoCache(true); + } +} + +/** + * Result of checking whether the current AI agent has Cloudflare skills installed. + * + * - `null` — no agent detected or the agent is not in the supported list. + * - `false` — agent is supported but no skills are present on disk. + * - `"automatic"` — skills are present and the metadata file confirms Wrangler installed them. + * - `"manual"` — skills are present but were not installed by Wrangler (no metadata, agent not + * listed in metadata, or the metadata records a failure for this agent). + */ +type AgentSkillsInstallStatus = "automatic" | "manual" | false | null; + +/** + * Maps an `am-i-vibing` agent ID to its global skills paths (both the rosie + * install path and any alternative paths the agent natively reads from). + * This is used by the telemetry function to check whether skills exist on + * disk without calling `rosie.agents()`, which loads WASM and is too slow + * for process-startup telemetry. + */ +interface AmIVibingDataMappingEntry { + /** `am-i-vibing` provider ID(s) that identify this agent. */ + amIVibingIds: string[]; + /** + * Rosie agent metadata. + * + * The `globalPath` is the absolute path to the agent's global skills + * directory, constructed via `path.join(os.homedir(), ...)` to match + * the `global_path` field in rosie's `AGENT_DEFS` registry. + * See: https://github.com/matthewp/rosie/blob/276939233/src/agent.rs#L26 + */ + rosie: { globalPath: string }; + /** + * Additional absolute global skills paths where this agent natively + * reads skills from, beyond the rosie install path. Used to detect + * manual skill installations for telemetry. Only paths that differ + * from `rosie.globalPath` should be listed here. + * + * Sources for each agent's alternative paths are linked inline below. + */ + alternativeGlobalPaths?: string[]; +} + +/** + * Static mapping from `am-i-vibing` agent IDs to their global skills paths, + * used only for telemetry. + * + * The `amIVibingIds` are the provider IDs returned by the `am-i-vibing` + * package's `detectAgenticEnvironment()` function. + * + * The `rosie.globalPath` values correspond to the `global_path` field from + * rosie's agent registry, constructed as absolute paths via + * `path.join(os.homedir(), ...)`: + * https://github.com/matthewp/rosie/blob/2769392335/src/agent.rs#L26 + * + * The `alternativeGlobalPaths` are additional absolute directories that + * each agent natively reads skills from, sourced from the agent's official + * documentation. These are used to detect manual skill installations for + * telemetry. + */ +const amIVibingDataMapping: AmIVibingDataMappingEntry[] = [ + { + // rosie name: "replit" / "amp" / "kimi-cli" / "universal" + amIVibingIds: ["replit", "replit-assistant"], + rosie: { + globalPath: path.join(os.homedir(), ".config", "agents", "skills"), + }, + // https://ampcode.com/manual#agent-skills + alternativeGlobalPaths: [ + path.join(os.homedir(), ".config", "amp", "skills"), + path.join(os.homedir(), ".claude", "skills"), + ], + }, + { + // rosie name: "claude" + // https://code.claude.com/docs/en/skills — only ~/.claude/skills/ documented + amIVibingIds: ["claude-code"], + rosie: { globalPath: path.join(os.homedir(), ".claude", "skills") }, + }, + { + // rosie name: "warp" + // .agents/skills IS the rosie path; closed source, no other paths confirmed + amIVibingIds: ["warp"], + rosie: { globalPath: path.join(os.homedir(), ".agents", "skills") }, + }, + { + // rosie name: "codex" + amIVibingIds: ["codex"], + rosie: { globalPath: path.join(os.homedir(), ".codex", "skills") }, + // https://developers.openai.com/codex/skills — $HOME/.agents/skills is the USER scope + alternativeGlobalPaths: [path.join(os.homedir(), ".agents", "skills")], + }, + { + // rosie name: "crush" + amIVibingIds: ["crush"], + rosie: { + globalPath: path.join(os.homedir(), ".config", "crush", "skills"), + }, + // https://github.com/charmbracelet/crush?tab=readme-ov-file#agent-skills + alternativeGlobalPaths: [ + path.join(os.homedir(), ".config", "agents", "skills"), + path.join(os.homedir(), ".agents", "skills"), + path.join(os.homedir(), ".claude", "skills"), + ], + }, + { + // rosie name: "cursor" + amIVibingIds: ["cursor-agent", "cursor"], + rosie: { globalPath: path.join(os.homedir(), ".cursor", "skills") }, + // https://cursor.com/docs/context/skills + alternativeGlobalPaths: [ + path.join(os.homedir(), ".agents", "skills"), + path.join(os.homedir(), ".claude", "skills"), + path.join(os.homedir(), ".codex", "skills"), + ], + }, + { + // rosie name: "gemini-cli" + amIVibingIds: ["gemini-agent"], + rosie: { globalPath: path.join(os.homedir(), ".gemini", "skills") }, + // https://github.com/google-gemini/gemini-cli — Storage.getUserAgentSkillsDir() + alternativeGlobalPaths: [path.join(os.homedir(), ".agents", "skills")], + }, + { + // rosie name: "copilot" + amIVibingIds: ["vscode-copilot-agent"], + rosie: { globalPath: path.join(os.homedir(), ".copilot", "skills") }, + // https://docs.github.com/en/copilot/concepts/agents/about-agent-skills + alternativeGlobalPaths: [path.join(os.homedir(), ".agents", "skills")], + }, + { + // rosie name: "opencode" + amIVibingIds: ["opencode"], + rosie: { + globalPath: path.join(os.homedir(), ".config", "opencode", "skills"), + }, + // https://opencode.ai/docs/skills + alternativeGlobalPaths: [ + path.join(os.homedir(), ".opencode", "skills"), + path.join(os.homedir(), ".claude", "skills"), + path.join(os.homedir(), ".agents", "skills"), + ], + }, + { + // rosie name: "windsurf" + // Closed source, no alternative global paths confirmed + amIVibingIds: ["windsurf"], + rosie: { + globalPath: path.join(os.homedir(), ".codeium", "windsurf", "skills"), + }, + }, +]; + +/** + * Checks whether a directory contains at least one of the given skill names. + * + * @param dirPath - Absolute path to a skills directory. + * @param skillNames - Skill directory names to look for. + * @returns `true` if at least one skill name is present, `false` otherwise + * (including when the directory does not exist or is unreadable). + */ +function directoryContainsAnySkill( + dirPath: string, + skillNames: string[] +): boolean { + try { + const entries = new Set(readdirSync(dirPath)); + return skillNames.some((name) => entries.has(name)); + } catch { + return false; + } +} + +/** + * Determines whether the currently-running AI coding agent has Cloudflare + * skills installed. This runs asynchronously and is intended to be started + * eagerly on process startup so the result is available by the time + * telemetry events are dispatched. + * + * @returns The {@link AgentSkillsInstallStatus} for the current agent. + */ +async function computeTelemetryCurrentAgentSkillsInstalled(): Promise { + let agentId: string | null = null; + try { + const detection = detectAgenticEnvironment(process.env, []); + agentId = detection.id; + } catch { + return null; + } + if (!agentId) { + return null; + } + + const mapping = amIVibingDataMapping.find((a) => + a.amIVibingIds.includes(agentId) + ); + if (!mapping) { + return null; + } + + const globalSkillsPath = path.resolve(mapping.rosie.globalPath); + + const skillNames = await fetchSkillNamesFromGitHub(); + if (!skillNames || skillNames.length === 0) { + return false; + } + + // Check whether any Cloudflare skill exists at the primary rosie path. + const hasSkillsAtPrimary = directoryContainsAnySkill( + globalSkillsPath, + skillNames + ); + + if (!hasSkillsAtPrimary) { + // Skills not at the primary path — check alternative global paths that + // this agent natively reads from. + const matchedAlternativePath = (mapping.alternativeGlobalPaths ?? []).find( + (altPath) => directoryContainsAnySkill(path.resolve(altPath), skillNames) + ); + if (!matchedAlternativePath) { + return false; + } + + // Skills exist at an alternative path. Check whether Wrangler + // installed them there for a different agent whose rosie globalPath + // happens to match this alternative path (e.g. OpenCode reads + // ~/.agents/skills, which is Warp's rosie install target). + const metadata = readSkillsInstallMetadataFile(); + if (metadata?.accepted) { + const altAbsPath = path.resolve(matchedAlternativePath); + const wasInstalledForAnotherAgent = metadata.detectedAgents?.some( + (agent) => { + if (path.resolve(agent.rosie.globalPath) !== altAbsPath) { + return false; + } + // Ensure the install didn't fail for that agent. + if (metadata.installFailed === true) { + return false; + } + if (Array.isArray(metadata.installFailed)) { + return !metadata.installFailed.includes(agent.rosie.id); + } + return true; + } + ); + if (wasInstalledForAnotherAgent) { + return "automatic"; + } + } + return "manual"; + } + + const metadata = readSkillsInstallMetadataFile(); + if (!metadata) { + return "manual"; + } + + // Check if this agent was part of the Wrangler-driven install. + const isInDetectedAgents = metadata.detectedAgents?.some( + (agent) => path.resolve(agent.rosie.globalPath) === globalSkillsPath + ); + + // Check if the install failed for this agent. + // installFailed is `true` when the whole rosie.install() threw, `string[]` with + // rosie agent names on partial failure, or `false` on success. + const installFailedForAgent = + metadata.installFailed === true || + (Array.isArray(metadata.installFailed) && + metadata.detectedAgents?.some( + (agent) => + path.resolve(agent.rosie.globalPath) === globalSkillsPath && + (metadata.installFailed as string[]).includes(agent.rosie.id) + )); + + if ( + metadata.accepted && + isInDetectedAgents === true && + !installFailedForAgent + ) { + return "automatic"; + } + return "manual"; +} + +/** Lazily-initialised singleton promise used to memoise {@link telemetryCurrentAgentSkillsInstalled}. */ +let _telemetryPromise: Promise | undefined; + +/** + * Returns a memoised promise that resolves to whether the current AI agent + * has Cloudflare skills installed. The promise is started lazily on first + * call and cached for the lifetime of the process. + * + * @returns A promise resolving to the {@link AgentSkillsInstallStatus} for the current agent. + */ +export function telemetryCurrentAgentSkillsInstalled(): Promise { + _telemetryPromise ??= computeTelemetryCurrentAgentSkillsInstalled(); + return _telemetryPromise; +} + /** * Queries rosie for detected agents on the user's machine. * @@ -262,7 +746,9 @@ async function getDetectedAgents(): Promise { ) .map((agent) => ({ name: agent.display, - rosieId: agent.name, - globalSkillsPath: agent.installPath, + rosie: { + id: agent.name, + globalPath: agent.installPath, + }, })); } diff --git a/packages/wrangler/src/metrics/metrics-dispatcher.ts b/packages/wrangler/src/metrics/metrics-dispatcher.ts index 46340ad7d1..2f15388029 100644 --- a/packages/wrangler/src/metrics/metrics-dispatcher.ts +++ b/packages/wrangler/src/metrics/metrics-dispatcher.ts @@ -3,6 +3,7 @@ import { detectAgenticEnvironment } from "am-i-vibing"; import chalk from "chalk"; import ci from "ci-info"; import { fetch } from "undici"; +import { telemetryCurrentAgentSkillsInstalled } from "../agents-skills-install"; import isInteractive from "../is-interactive"; import { logger } from "../logger"; import { sniffUserAgent } from "../package-manager"; @@ -96,20 +97,34 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) { * - additional properties are camelCased */ sendAdhocEvent(name: string, properties: Properties = {}) { - dispatch({ - name, - properties: { - ...getCommonEventProperties(), - category: "Workers", - wranglerVersion, - wranglerMajorVersion, - wranglerMinorVersion, - wranglerPatchVersion, - os: getOS(), - agent, - ...properties, - }, - }); + trackDispatch( + telemetryCurrentAgentSkillsInstalled() + .catch(() => null) + .then((currentAgentSkillsInstalled) => { + const baseProperties = { + ...getCommonEventProperties(), + category: "Workers", + wranglerVersion, + wranglerMajorVersion, + wranglerMinorVersion, + wranglerPatchVersion, + os: getOS(), + agent, + ...properties, + }; + + return dispatch({ + name, + properties: { + ...baseProperties, + currentAgentSkillsInstalled, + }, + }); + }) + .catch((err) => { + logger.debug("Error sending adhoc metrics event", err); + }) + ); }, /** @@ -146,20 +161,25 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) { argsCombination, }; - dispatch({ - name, - properties: { - ...commonCommandEventProperties, - ...properties, - }, - }); + trackDispatch( + dispatch({ + name, + properties: { + ...commonCommandEventProperties, + ...properties, + }, + }) + ); } catch (err) { logger.debug("Error sending metrics event", err); } }, }; - function dispatch(event: { name: string; properties: Properties }) { + function dispatch(event: { + name: string; + properties: Properties; + }): Promise | void { const metricsConfig = getMetricsConfig(options); const body = { deviceId: metricsConfig.deviceId, @@ -191,7 +211,7 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) { // before exiting Wrangler. The exit handler in index.ts races // allMetricsDispatchesCompleted() against a 1s timeout, which is the // real protection against slow connections at shutdown. - const request = fetch(`${SPARROW_URL}/api/v1/event`, { + return fetch(`${SPARROW_URL}/api/v1/event`, { method: "POST", headers: { Accept: "*/*", @@ -215,12 +235,23 @@ export function getMetricsDispatcher(options: MetricsConfigOptions) { "Metrics dispatcher: Failed to send request:", (e as Error).message ); - }) - .finally(() => { - pendingRequests.delete(request); }); + } - pendingRequests.add(request); + /** + * Adds a dispatch promise (or promise chain) to `pendingRequests` so it + * is awaited at process exit. Automatically removes itself once settled. + * + * @param maybePromise The dispatch promise to track, or `void` if no async work is needed. + */ + function trackDispatch(maybePromise: Promise | void): void { + if (!maybePromise) { + return; + } + const tracked = maybePromise.finally(() => { + pendingRequests.delete(tracked); + }); + pendingRequests.add(tracked); } /** diff --git a/packages/wrangler/telemetry.md b/packages/wrangler/telemetry.md index f5979122c8..1fa565a64b 100644 --- a/packages/wrangler/telemetry.md +++ b/packages/wrangler/telemetry.md @@ -27,6 +27,7 @@ Telemetry in Wrangler allows us to better identify bugs and gain visibility on u - Total session duration of the command run (e.g. 3 seconds, etc.) - Whether the Wrangler client is running in CI or in an interactive instance - Whether the command was executed by an AI coding agent (e.g. Claude Code, Cursor, GitHub Copilot), and if so, which agent +- Whether the AI coding agent has Cloudflare skills installed, and if so, whether they were installed automatically by Wrangler or manually by the user - Error _type_ (e.g. `APIError` or `UserError`), and sanitised error messages that will not include user information like filepaths or stack traces (e.g. `Asset too large`). - General machine information such as OS and OS Version - local REST API usage (e.g. via the Local Explorer): From b3962ffadb4ce13dea543c994bf3f663e7d445a5 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Tue, 26 May 2026 15:03:31 +0100 Subject: [PATCH 7/7] Improve error messages for Pages CLI commands (#14010) --- .changeset/improve-pages-error-messages.md | 8 ++++++ .../pages/__snapshots__/deploy.test.ts.snap | 4 +-- .../src/__tests__/pages/deploy.test.ts | 2 +- .../wrangler/src/__tests__/pages/dev.test.ts | 6 ++-- .../__tests__/pages/pages-build-env.test.ts | 4 +-- .../pages/pages-deployment-tail.test.ts | 4 +-- .../pages/pages-download-config.test.ts | 2 +- .../src/__tests__/pages/secret.test.ts | 16 +++++++---- packages/wrangler/src/pages/build-env.ts | 17 +++++++---- packages/wrangler/src/pages/deploy.ts | 28 +++++++++++-------- .../wrangler/src/pages/deployment-tails.ts | 19 ++++++------- packages/wrangler/src/pages/deployments.ts | 20 +++++++------ packages/wrangler/src/pages/dev.ts | 25 +++++++++-------- .../wrangler/src/pages/download-config.ts | 11 +++++--- packages/wrangler/src/pages/projects.ts | 20 +++++++------ packages/wrangler/src/pages/secret/index.ts | 15 +++++----- packages/wrangler/src/pages/upload.ts | 9 +++--- packages/wrangler/src/pages/validate.ts | 10 +++---- 18 files changed, 125 insertions(+), 95 deletions(-) create mode 100644 .changeset/improve-pages-error-messages.md diff --git a/.changeset/improve-pages-error-messages.md b/.changeset/improve-pages-error-messages.md new file mode 100644 index 0000000000..da0be87fca --- /dev/null +++ b/.changeset/improve-pages-error-messages.md @@ -0,0 +1,8 @@ +--- +"wrangler": patch +--- + +Improve error messages for Pages CLI commands + +Error messages across `wrangler pages` subcommands (deploy, dev, secret, project, etc.) now provide clearer descriptions and actionable guidance. For example, instead of "Must specify a project name.", you'll now see "Missing Pages project name. Use --project-name or set the name in your wrangler.jsonc configuration file." + diff --git a/packages/wrangler/src/__tests__/pages/__snapshots__/deploy.test.ts.snap b/packages/wrangler/src/__tests__/pages/__snapshots__/deploy.test.ts.snap index f047b63bec..75bc062033 100644 --- a/packages/wrangler/src/__tests__/pages/__snapshots__/deploy.test.ts.snap +++ b/packages/wrangler/src/__tests__/pages/__snapshots__/deploy.test.ts.snap @@ -30,7 +30,7 @@ exports[`pages deploy > with wrangler.json configuration > should support wrangl ✨ Deployment complete! Take a peek over at https://abcxyz.pages-is-awesome.pages.dev/" `; -exports[`pages deploy > with wrangler.json configuration > should warn and ignore the config file, if it doesn't specify the \`pages_build_output_dir\` field 1`] = `[Error: Must specify a project name.]`; +exports[`pages deploy > with wrangler.json configuration > should warn and ignore the config file, if it doesn't specify the \`pages_build_output_dir\` field 1`] = `[Error: Missing Pages project name. Use --project-name or set the name in your Wrangler configuration file.]`; exports[`pages deploy > with wrangler.toml configuration > should always deploy to the Pages project specified by the top-level \`name\` configuration field, regardless of the corresponding env-level configuration 1`] = `"b5f53af7922efd0f0b73c7bf288f7e3f14df5c31e8b654e98027ddf85ea7e574"`; @@ -62,4 +62,4 @@ exports[`pages deploy > with wrangler.toml configuration > should support wrangl ✨ Deployment complete! Take a peek over at https://abcxyz.pages-is-awesome.pages.dev/" `; -exports[`pages deploy > with wrangler.toml configuration > should warn and ignore the config file, if it doesn't specify the \`pages_build_output_dir\` field 1`] = `[Error: Must specify a project name.]`; +exports[`pages deploy > with wrangler.toml configuration > should warn and ignore the config file, if it doesn't specify the \`pages_build_output_dir\` field 1`] = `[Error: Missing Pages project name. Use --project-name or set the name in your Wrangler configuration file.]`; diff --git a/packages/wrangler/src/__tests__/pages/deploy.test.ts b/packages/wrangler/src/__tests__/pages/deploy.test.ts index 3f43936421..490d8a0837 100644 --- a/packages/wrangler/src/__tests__/pages/deploy.test.ts +++ b/packages/wrangler/src/__tests__/pages/deploy.test.ts @@ -115,7 +115,7 @@ describe("pages deploy", () => { await expect( runWrangler("pages deploy public") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Must specify a project name.]` + `[Error: Missing Pages project name. Use --project-name or set the name in your Wrangler configuration file.]` ); }); diff --git a/packages/wrangler/src/__tests__/pages/dev.test.ts b/packages/wrangler/src/__tests__/pages/dev.test.ts index b7d58492b3..8c29507b23 100644 --- a/packages/wrangler/src/__tests__/pages/dev.test.ts +++ b/packages/wrangler/src/__tests__/pages/dev.test.ts @@ -27,7 +27,7 @@ describe("pages dev", () => { await expect( runWrangler("pages dev public -- yarn dev") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Specify either a directory OR a proxy command, not both.]` + `[Error: Cannot specify both a directory and a proxy command. Provide either a directory of static assets or a proxy command, not both.]` ); }); @@ -37,7 +37,7 @@ describe("pages dev", () => { await expect( runWrangler("pages dev public --config=/path/to/wrangler.toml") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Pages does not support custom paths for the Wrangler configuration file]` + `[Error: Pages does not support custom paths for the Wrangler configuration file. Remove the --config flag, or use a standard wrangler.jsonc in your project root.]` ); }); @@ -47,7 +47,7 @@ describe("pages dev", () => { await expect( runWrangler("pages dev public --env=production") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Pages does not support targeting an environment with the --env flag during local development.]` + `[Error: Pages does not support the --env flag during local development. Use the --branch flag to target your production or preview environment instead.]` ); }); }); diff --git a/packages/wrangler/src/__tests__/pages/pages-build-env.test.ts b/packages/wrangler/src/__tests__/pages/pages-build-env.test.ts index 3bc744fda2..1be3dd069d 100644 --- a/packages/wrangler/src/__tests__/pages/pages-build-env.test.ts +++ b/packages/wrangler/src/__tests__/pages/pages-build-env.test.ts @@ -49,7 +49,7 @@ describe("pages build env", () => { await expect( runWrangler("pages functions build-env") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: No Pages project location specified]` + `[Error: Missing Pages project location. Provide the project directory as a positional argument.]` ); }); @@ -57,7 +57,7 @@ describe("pages build env", () => { await expect( runWrangler("pages functions build-env .") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: No outfile specified]` + `[Error: Missing output file. Use --outfile to specify where to write the build environment configuration.]` ); }); diff --git a/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts b/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts index 7d774c559e..07b7d8d1f0 100644 --- a/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts +++ b/packages/wrangler/src/__tests__/pages/pages-deployment-tail.test.ts @@ -79,7 +79,7 @@ describe("pages deployment tail", () => { await expect( runWrangler("pages deployment tail") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Must specify a deployment in non-interactive mode.]` + `[Error: Missing deployment. In non-interactive mode, provide the deployment ID or URL as a positional argument.]` ); expect(api.requests.deployments.count).toStrictEqual(0); await api.closeHelper(); @@ -146,7 +146,7 @@ describe("pages deployment tail", () => { await expect( runWrangler("pages deployment tail foo") ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Must specify a project name in non-interactive mode.]` + `[Error: Missing Pages project name. In non-interactive mode, use --project-name to specify which project to tail.]` ); }); diff --git a/packages/wrangler/src/__tests__/pages/pages-download-config.test.ts b/packages/wrangler/src/__tests__/pages/pages-download-config.test.ts index 98182779a5..27396debf8 100644 --- a/packages/wrangler/src/__tests__/pages/pages-download-config.test.ts +++ b/packages/wrangler/src/__tests__/pages/pages-download-config.test.ts @@ -888,7 +888,7 @@ describe("pages download config", () => { await expect( runWrangler(`pages download config`) ).rejects.toThrowErrorMatchingInlineSnapshot( - `[Error: Must specify a project name.]` + `[Error: Missing Pages project name. Provide the project name as a positional argument: wrangler pages download config .]` ); }); it("should fail if project does not exist", async ({ expect }) => { diff --git a/packages/wrangler/src/__tests__/pages/secret.test.ts b/packages/wrangler/src/__tests__/pages/secret.test.ts index 192dbfcb2d..39ac3e91d8 100644 --- a/packages/wrangler/src/__tests__/pages/secret.test.ts +++ b/packages/wrangler/src/__tests__/pages/secret.test.ts @@ -161,7 +161,7 @@ describe("wrangler pages secret", () => { "pages secret put the-key --project some-project-name --env some-env" ) ).rejects.toMatchInlineSnapshot( - `[Error: Pages does not support the "some-env" named environment. Please specify "production" (default) or "preview"]` + `[Error: Pages does not support the "some-env" environment. Only "production" and "preview" are valid. Use --env production or --env preview.]` ); }); @@ -169,7 +169,7 @@ describe("wrangler pages secret", () => { await expect( runWrangler("pages secret put the-key") ).rejects.toMatchInlineSnapshot( - `[Error: Must specify a project name.]` + `[Error: Missing Pages project name. Use --project-name to specify which project to manage secrets for.]` ); }); }); @@ -398,14 +398,16 @@ describe("wrangler pages secret", () => { "pages secret delete the-key --project some-project-name --env some-env" ) ).rejects.toMatchInlineSnapshot( - `[Error: Pages does not support the "some-env" named environment. Please specify "production" (default) or "preview"]` + `[Error: Pages does not support the "some-env" environment. Only "production" and "preview" are valid. Use --env production or --env preview.]` ); }); it("should error without a project name", async ({ expect }) => { await expect( runWrangler("pages secret delete the-key") - ).rejects.toMatchInlineSnapshot(`[Error: Must specify a project name.]`); + ).rejects.toMatchInlineSnapshot( + `[Error: Missing Pages project name. Use --project-name to specify which project to manage secrets for.]` + ); }); }); @@ -487,14 +489,16 @@ describe("wrangler pages secret", () => { "pages secret list --project some-project-name --env some-env" ) ).rejects.toMatchInlineSnapshot( - `[Error: Pages does not support the "some-env" named environment. Please specify "production" (default) or "preview"]` + `[Error: Pages does not support the "some-env" environment. Only "production" and "preview" are valid. Use --env production or --env preview.]` ); }); it("should error without a project name", async ({ expect }) => { await expect( runWrangler("pages secret list") - ).rejects.toMatchInlineSnapshot(`[Error: Must specify a project name.]`); + ).rejects.toMatchInlineSnapshot( + `[Error: Missing Pages project name. Use --project-name to specify which project to manage secrets for.]` + ); }); }); diff --git a/packages/wrangler/src/pages/build-env.ts b/packages/wrangler/src/pages/build-env.ts index aed3bac7a2..6e44c1612c 100644 --- a/packages/wrangler/src/pages/build-env.ts +++ b/packages/wrangler/src/pages/build-env.ts @@ -4,6 +4,7 @@ import { configFileName, FatalError, findWranglerConfig, + UserError, } from "@cloudflare/workers-utils"; import { readPagesConfig } from "../config"; import { createCommand } from "../core/create-command"; @@ -37,14 +38,18 @@ export const pagesFunctionsBuildEnvCommand = createCommand({ positionalArgs: ["projectDir"], async handler(args) { if (!args.projectDir) { - throw new FatalError("No Pages project location specified", { - telemetryMessage: "pages build env missing project directory", - }); + throw new UserError( + "Missing Pages project location. Provide the project directory as a positional argument.", + { + telemetryMessage: "pages build env missing project directory", + } + ); } if (!args.outfile) { - throw new FatalError("No outfile specified", { - telemetryMessage: "pages build env missing outfile", - }); + throw new UserError( + "Missing output file. Use --outfile to specify where to write the build environment configuration.", + { telemetryMessage: "pages build env missing outfile" } + ); } logger.log( diff --git a/packages/wrangler/src/pages/deploy.ts b/packages/wrangler/src/pages/deploy.ts index ceb620d18c..bf94fb3b35 100644 --- a/packages/wrangler/src/pages/deploy.ts +++ b/packages/wrangler/src/pages/deploy.ts @@ -276,10 +276,12 @@ export const pagesDeployCommand = createCommand({ projectName = await prompt("Enter the name of your new project:"); if (!projectName) { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages deploy missing project name", - }); + throw new UserError( + "Missing Pages project name. Use --project-name or set the name in your Wrangler configuration file.", + { + telemetryMessage: "pages deploy missing project name", + } + ); } } @@ -322,10 +324,12 @@ export const pagesDeployCommand = createCommand({ }); if (!productionBranch) { - throw new FatalError("Must specify a production branch.", { - code: 1, - telemetryMessage: "pages deploy missing production branch", - }); + throw new UserError( + "Missing production branch. Specify the production branch for your new Pages project when prompted, or re-run with the required information.", + { + telemetryMessage: "pages deploy missing production branch", + } + ); } await fetchResult( @@ -353,10 +357,10 @@ export const pagesDeployCommand = createCommand({ } if (!projectName) { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages deploy missing project name", - }); + throw new UserError( + "Missing Pages project name. Use --project-name or set the name in your Wrangler configuration file.", + { telemetryMessage: "pages deploy missing project name" } + ); } // We infer git info by default is not passed in diff --git a/packages/wrangler/src/pages/deployment-tails.ts b/packages/wrangler/src/pages/deployment-tails.ts index b8856b2177..2b9eb240a3 100644 --- a/packages/wrangler/src/pages/deployment-tails.ts +++ b/packages/wrangler/src/pages/deployment-tails.ts @@ -2,6 +2,7 @@ import { setTimeout } from "node:timers/promises"; import { COMPLIANCE_REGION_CONFIG_PUBLIC, FatalError, + UserError, } from "@cloudflare/workers-utils"; import onExit from "signal-exit"; import { fetchResult } from "../cfetch"; @@ -149,20 +150,18 @@ export const pagesDeploymentTailCommand = createCommand({ if (!isInteractive()) { if (!deploymentId) { - throw new FatalError( - "Must specify a deployment in non-interactive mode.", + throw new UserError( + "Missing deployment. In non-interactive mode, provide the deployment ID or URL as a positional argument.", { - code: 1, telemetryMessage: "pages deployment tail missing deployment", } ); } if (!projectName) { - throw new FatalError( - "Must specify a project name in non-interactive mode.", + throw new UserError( + "Missing Pages project name. In non-interactive mode, use --project-name to specify which project to tail.", { - code: 1, telemetryMessage: "pages deployment tail missing project name", } ); @@ -174,10 +173,10 @@ export const pagesDeploymentTailCommand = createCommand({ } if (!deployment && !projectName) { - throw new FatalError("Must specify a project name or deployment.", { - code: 1, - telemetryMessage: "pages deployment tail missing target", - }); + throw new UserError( + "Missing target. Provide a deployment ID or URL as a positional argument, or use --project-name to specify which project to tail.", + { telemetryMessage: "pages deployment tail missing target" } + ); } const deployments: Array = await fetchResult( diff --git a/packages/wrangler/src/pages/deployments.ts b/packages/wrangler/src/pages/deployments.ts index cc70ed2b96..6f8b47d801 100644 --- a/packages/wrangler/src/pages/deployments.ts +++ b/packages/wrangler/src/pages/deployments.ts @@ -1,6 +1,6 @@ import { COMPLIANCE_REGION_CONFIG_PUBLIC, - FatalError, + UserError, } from "@cloudflare/workers-utils"; import { format as timeagoFormat } from "timeago.js"; import { fetchResult } from "../cfetch"; @@ -60,10 +60,10 @@ export const pagesDeploymentListCommand = createCommand({ } if (!projectName) { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages deployments list missing project name", - }); + throw new UserError( + "Missing Pages project name. Use --project-name to specify which project to list deployments for.", + { telemetryMessage: "pages deployments list missing project name" } + ); } const deployments: Array = await fetchResult( @@ -160,10 +160,12 @@ export const pagesDeploymentDeleteCommand = createCommand({ } if (!projectName) { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages deployments delete missing project name", - }); + throw new UserError( + "Missing Pages project name. Use --project-name to specify which project to delete the deployment from.", + { + telemetryMessage: "pages deployments delete missing project name", + } + ); } const confirmed = diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index f8208706bf..7ebd1d3aa4 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -325,16 +325,16 @@ export const pagesDevCommand = createCommand({ } if (args.config && !Array.isArray(args.config)) { - throw new FatalError( - "Pages does not support custom paths for the Wrangler configuration file", - { code: 1, telemetryMessage: "pages dev custom config unsupported" } + throw new UserError( + "Pages does not support custom paths for the Wrangler configuration file. Remove the --config flag, or use a standard wrangler.jsonc in your project root.", + { telemetryMessage: "pages dev custom config unsupported" } ); } if (args.env) { - throw new FatalError( - "Pages does not support targeting an environment with the --env flag during local development.", - { code: 1, telemetryMessage: "pages dev env unsupported" } + throw new UserError( + "Pages does not support the --env flag during local development. Use the --branch flag to target your production or preview environment instead.", + { telemetryMessage: "pages dev env unsupported" } ); } @@ -357,9 +357,10 @@ export const pagesDevCommand = createCommand({ config.configPath && path.resolve(process.cwd(), args.config[0]) !== config.configPath ) { - throw new FatalError( - "The first `--config` argument must point to your Pages configuration file: " + - path.relative(process.cwd(), config.configPath), + throw new UserError( + "The first `--config` argument must point to your Pages configuration file. Expected: " + + path.relative(process.cwd(), config.configPath) + + ". Ensure the file exists and is a valid Pages configuration.", { telemetryMessage: "pages dev config path mismatch" } ); } @@ -370,9 +371,9 @@ export const pagesDevCommand = createCommand({ let directory = resolvedDirectory; if (directory !== undefined && command.length > 0) { - throw new FatalError( - "Specify either a directory OR a proxy command, not both.", - { code: 1, telemetryMessage: "pages dev conflicting serve targets" } + throw new UserError( + "Cannot specify both a directory and a proxy command. Provide either a directory of static assets or a proxy command, not both.", + { telemetryMessage: "pages dev conflicting serve targets" } ); } else if (directory === undefined) { proxyPort = await spawnProxyProcess({ diff --git a/packages/wrangler/src/pages/download-config.ts b/packages/wrangler/src/pages/download-config.ts index 55dcf30363..4b13c2d9f9 100644 --- a/packages/wrangler/src/pages/download-config.ts +++ b/packages/wrangler/src/pages/download-config.ts @@ -4,6 +4,7 @@ import { COMPLIANCE_REGION_CONFIG_PUBLIC, FatalError, getTodaysCompatDate, + UserError, } from "@cloudflare/workers-utils"; import chalk from "chalk"; import TOML from "smol-toml"; @@ -325,10 +326,12 @@ export const pagesDownloadConfigCommand = createCommand({ projectName ??= projectConfig.project_name; if (!projectName) { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages download config missing project name", - }); + throw new UserError( + "Missing Pages project name. Provide the project name as a positional argument: wrangler pages download config .", + { + telemetryMessage: "pages download config missing project name", + } + ); } const config = await downloadProject(accountId, projectName); if (!force && existsSync("wrangler.toml")) { diff --git a/packages/wrangler/src/pages/projects.ts b/packages/wrangler/src/pages/projects.ts index a2620222ff..898e4b4923 100644 --- a/packages/wrangler/src/pages/projects.ts +++ b/packages/wrangler/src/pages/projects.ts @@ -1,7 +1,7 @@ import { execSync } from "node:child_process"; import { COMPLIANCE_REGION_CONFIG_PUBLIC, - FatalError, + UserError, } from "@cloudflare/workers-utils"; import { format as timeagoFormat } from "timeago.js"; import { fetchResult } from "../cfetch"; @@ -143,10 +143,10 @@ export const pagesProjectCreateCommand = createCommand({ } if (!projectName) { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages projects create missing project name", - }); + throw new UserError( + "Missing Pages project name. Provide the project name as a positional argument: wrangler pages project create .", + { telemetryMessage: "pages projects create missing project name" } + ); } if (!productionBranch && isInteractive) { @@ -187,10 +187,12 @@ export const pagesProjectCreateCommand = createCommand({ } if (!productionBranch) { - throw new FatalError("Must specify a production branch.", { - code: 1, - telemetryMessage: "pages projects create missing production branch", - }); + throw new UserError( + "Missing production branch. Use --production-branch to specify the production branch for your Pages project.", + { + telemetryMessage: "pages projects create missing production branch", + } + ); } const deploymentConfig = { diff --git a/packages/wrangler/src/pages/secret/index.ts b/packages/wrangler/src/pages/secret/index.ts index 05b2bd8d7f..5398a9b48f 100644 --- a/packages/wrangler/src/pages/secret/index.ts +++ b/packages/wrangler/src/pages/secret/index.ts @@ -3,6 +3,7 @@ import { configFileName, FatalError, findWranglerConfig, + UserError, } from "@cloudflare/workers-utils"; import chalk from "chalk"; import { fetchResult } from "../../cfetch"; @@ -38,9 +39,9 @@ async function pagesProject( }> { env ??= "production"; if (!isPagesEnv(env)) { - throw new FatalError( - `Pages does not support the "${env}" named environment. Please specify "production" (default) or "preview"`, - { code: 1, telemetryMessage: "pages secret env unsupported" } + throw new UserError( + `Pages does not support the "${env}" environment. Only "production" and "preview" are valid. Use --env production or --env preview.`, + { telemetryMessage: "pages secret env unsupported" } ); } let config: Config | undefined; @@ -105,10 +106,10 @@ async function pagesProject( throw err; } } else { - throw new FatalError("Must specify a project name.", { - code: 1, - telemetryMessage: "pages secret missing project name", - }); + throw new UserError( + "Missing Pages project name. Use --project-name to specify which project to manage secrets for.", + { telemetryMessage: "pages secret missing project name" } + ); } return { env, project, accountId, config }; } diff --git a/packages/wrangler/src/pages/upload.ts b/packages/wrangler/src/pages/upload.ts index 0bce9b2360..fb1893ba08 100644 --- a/packages/wrangler/src/pages/upload.ts +++ b/packages/wrangler/src/pages/upload.ts @@ -5,6 +5,7 @@ import { APIError, COMPLIANCE_REGION_CONFIG_PUBLIC, FatalError, + UserError, } from "@cloudflare/workers-utils"; import PQueue from "p-queue"; import { fetchResult } from "../cfetch"; @@ -53,10 +54,10 @@ export const pagesProjectUploadCommand = createCommand({ positionalArgs: ["directory"], async handler({ directory, outputManifestPath, skipCaching }) { if (!directory) { - throw new FatalError("Must specify a directory.", { - code: 1, - telemetryMessage: "pages upload missing directory", - }); + throw new UserError( + "Missing directory. Provide the path to your build output directory as a positional argument.", + { telemetryMessage: "pages upload missing directory" } + ); } if (!process.env.CF_PAGES_UPLOAD_JWT) { diff --git a/packages/wrangler/src/pages/validate.ts b/packages/wrangler/src/pages/validate.ts index c0381a2f9b..b3583d8fc2 100644 --- a/packages/wrangler/src/pages/validate.ts +++ b/packages/wrangler/src/pages/validate.ts @@ -1,6 +1,6 @@ import { readdir, stat } from "node:fs/promises"; import { join, relative, resolve, sep } from "node:path"; -import { FatalError } from "@cloudflare/workers-utils"; +import { FatalError, UserError } from "@cloudflare/workers-utils"; import { getType } from "mime"; import { Minimatch } from "minimatch"; import prettyBytes from "pretty-bytes"; @@ -29,10 +29,10 @@ export const pagesProjectValidateCommand = createCommand({ positionalArgs: ["directory"], async handler({ directory }) { if (!directory) { - throw new FatalError("Must specify a directory.", { - code: 1, - telemetryMessage: "pages validate missing directory", - }); + throw new UserError( + "Missing directory. Provide the path to the directory to validate as a positional argument.", + { telemetryMessage: "pages validate missing directory" } + ); } const fileCountLimit = process.env.CF_PAGES_UPLOAD_JWT