diff --git a/.changeset/dirty-moments-join.md b/.changeset/dirty-moments-join.md new file mode 100644 index 0000000000..8e840e22b4 --- /dev/null +++ b/.changeset/dirty-moments-join.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Re-export `supportedCompatibilityDate` from miniflare + +The re-exports contains a recent and safe compatibility date from the local version of `workerd`. diff --git a/.changeset/long-colts-occur.md b/.changeset/long-colts-occur.md new file mode 100644 index 0000000000..2bd261bb79 --- /dev/null +++ b/.changeset/long-colts-occur.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/vite-plugin": patch +--- + +Use `supportedCompatibilityDate` value from `miniflare` instead of getting the date from `@cloudflare/workers-utils` + +`miniflare` exports a recent safe compatibility date as `supportedCompatibilityDate`, and that is the value the package now uses as the latest supported workerd compatibility date. This doesn't have any specific user-facing effect (besides potentially making the function more reliable). diff --git a/.changeset/quiet-roses-help.md b/.changeset/quiet-roses-help.md new file mode 100644 index 0000000000..43a44b31cd --- /dev/null +++ b/.changeset/quiet-roses-help.md @@ -0,0 +1,9 @@ +--- +"create-cloudflare": patch +--- + +Avoid resorting to the fallback compatibility date when running via `pnpm` + +Previously, when running `create-cloudflare` via `pnpm` the resolution of the local `miniflare` package failed and the fallback date was always used instead of the actual `workerd` compatibility date. + +The fix switches to using `supportedCompatibilityDate` from `wrangler` directly, which does not depend on the `miniflare` resolution and works reliably across all package managers. diff --git a/.changeset/smart-buckets-wonder.md b/.changeset/smart-buckets-wonder.md new file mode 100644 index 0000000000..6e84eaf29e --- /dev/null +++ b/.changeset/smart-buckets-wonder.md @@ -0,0 +1,12 @@ +--- +"@cloudflare/workers-utils": minor +--- + +Remove the `getLocalWorkerdCompatibilityDate` utility from the package + +This utility has been removed because its implementation relied on retrieving the compat date from `workerd` by resolving to the `miniflare` dependency, which was unreliable in certain environments (e.g. when using `pnpm`). The functionality is now provided more reliably as a static export from `wrangler`. + +Consumers should migrate to one of the following alternatives: + +- `supportedCompatibilityDate` from `miniflare` — for direct miniflare users +- `supportedCompatibilityDate` re-exported from `wrangler` — for consumers of the wrangler programmatic API diff --git a/packages/create-cloudflare/e2e/tests/workers/workers.test.ts b/packages/create-cloudflare/e2e/tests/workers/workers.test.ts index 5a1be7e019..dd31c809cc 100644 --- a/packages/create-cloudflare/e2e/tests/workers/workers.test.ts +++ b/packages/create-cloudflare/e2e/tests/workers/workers.test.ts @@ -1,5 +1,6 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; +import { FALLBACK_COMPAT_DATE } from "helpers/compatDate"; import { readJSON, readToml } from "helpers/files"; import { beforeAll, describe } from "vitest"; import { deleteWorker } from "../../../scripts/common"; @@ -63,15 +64,33 @@ describe const jsoncPath = join(project.path, "wrangler.jsonc"); if (existsSync(jsoncPath)) { - const config = readJSON(jsoncPath) as { main?: string }; + const config = readJSON(jsoncPath) as { + main?: string; + compatibility_date?: string; + }; if (config.main) { expect(join(project.path, config.main)).toExist(); } + // Verify the compatibility_date was resolved from the locally + // installed wrangler package and it isn't the hardcoded fallback. + expect(config.compatibility_date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(config.compatibility_date).not.toEqual( + FALLBACK_COMPAT_DATE + ); } else if (existsSync(tomlPath)) { - const config = readToml(tomlPath) as { main?: string }; + const config = readToml(tomlPath) as { + main?: string; + compatibility_date?: string; + }; if (config.main) { expect(join(project.path, config.main)).toExist(); } + // Verify the compatibility_date was resolved from the locally + // installed wrangler package and it isn't the hardcoded fallback. + expect(config.compatibility_date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(config.compatibility_date).not.toEqual( + FALLBACK_COMPAT_DATE + ); } else { expect.fail( `Expected at least one of "${jsoncPath}" or "${tomlPath}" to exist.` diff --git a/packages/create-cloudflare/src/helpers/__tests__/compatDate.test.ts b/packages/create-cloudflare/src/helpers/__tests__/compatDate.test.ts index a3bb9d5c35..1e7e095235 100644 --- a/packages/create-cloudflare/src/helpers/__tests__/compatDate.test.ts +++ b/packages/create-cloudflare/src/helpers/__tests__/compatDate.test.ts @@ -1,5 +1,6 @@ -import { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; +import { createRequire } from "node:module"; import { + FALLBACK_COMPAT_DATE, getLatestTypesEntrypoint, getWorkerdCompatibilityDate, } from "helpers/compatDate"; @@ -10,7 +11,7 @@ import { mockSpinner, mockWorkersTypesDirectory } from "./mocks"; vi.mock("helpers/files"); vi.mock("fs"); vi.mock("@cloudflare/cli/interactive"); -vi.mock("@cloudflare/workers-utils"); +vi.mock("node:module"); describe("Compatibility Date Helpers", () => { let spinner: ReturnType; @@ -25,10 +26,12 @@ describe("Compatibility Date Helpers", () => { describe("getWorkerdCompatibilityDate()", () => { test("normal flow", async ({ expect }) => { - vi.mocked(getLocalWorkerdCompatibilityDate).mockReturnValue({ - date: "2025-01-10", - source: "workerd", - }); + const mockWrangler = { + supportedCompatibilityDate: "2025-01-10", + }; + vi.mocked(createRequire).mockReturnValue( + (() => mockWrangler) as unknown as NodeJS.Require + ); const date = getWorkerdCompatibilityDate("./my-app"); @@ -40,19 +43,35 @@ describe("Compatibility Date Helpers", () => { ); }); - test("fallback result", async ({ expect }) => { - vi.mocked(getLocalWorkerdCompatibilityDate).mockReturnValue({ - date: "2025-09-27", - source: "fallback", - }); + test("fallback on error", async ({ expect }) => { + vi.mocked(createRequire).mockReturnValue((() => { + throw new Error("Cannot find module 'wrangler'"); + }) as unknown as NodeJS.Require); + + const date = getWorkerdCompatibilityDate("./my-app"); + + expect(date).toBe(FALLBACK_COMPAT_DATE); + expect(spinner.start).toHaveBeenCalled(); + expect(spinner.stop).toHaveBeenCalledWith( + expect.stringContaining(FALLBACK_COMPAT_DATE) + ); + }); + + test("fallback when wrangler does not export supportedCompatibilityDate (older version)", async ({ + expect, + }) => { + // Simulate an older version of wrangler that doesn't have this export + const mockWrangler = {}; + vi.mocked(createRequire).mockReturnValue( + (() => mockWrangler) as unknown as NodeJS.Require + ); const date = getWorkerdCompatibilityDate("./my-app"); - const fallbackDate = "2025-09-27"; - expect(date).toBe(fallbackDate); + expect(date).toBe(FALLBACK_COMPAT_DATE); expect(spinner.start).toHaveBeenCalled(); expect(spinner.stop).toHaveBeenCalledWith( - expect.stringContaining(fallbackDate) + expect.stringContaining(FALLBACK_COMPAT_DATE) ); }); }); diff --git a/packages/create-cloudflare/src/helpers/compatDate.ts b/packages/create-cloudflare/src/helpers/compatDate.ts index 6ccff0bd26..c3ee91d041 100644 --- a/packages/create-cloudflare/src/helpers/compatDate.ts +++ b/packages/create-cloudflare/src/helpers/compatDate.ts @@ -1,31 +1,56 @@ import { readdirSync } from "node:fs"; -import { resolve } from "node:path"; +import { createRequire } from "node:module"; +import { join, resolve } from "node:path"; import { brandColor, dim } from "@cloudflare/cli/colors"; import { spinner } from "@cloudflare/cli/interactive"; -import { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; import type { C3Context } from "types"; +import type { CompatDate } from "wrangler"; + +/** + * Compatibility date to fallback to if getting the compatibility date from wrangler fails for whatever reason. + * + * Note: this fallback date doesn't have any special meaning, it's simply the latest compatibility date at the time of writing + * (source: https://github.com/cloudflare/workerd/blob/main/src/workerd/io/release-version.txt#L1) + */ +export const FALLBACK_COMPAT_DATE = "2026-03-24"; /** * Retrieves the latest workerd compatibility date * * @returns The latest compatibility date for workerd in the form "YYYY-MM-DD" */ -export function getWorkerdCompatibilityDate(projectPath: string) { +export function getWorkerdCompatibilityDate(projectPath: string): CompatDate { const s = spinner(); s.start("Retrieving current workerd compatibility date"); - const { date, source } = getLocalWorkerdCompatibilityDate({ projectPath }); + try { + const projectRequire = createRequire(join(projectPath, "package.json")); + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const wrangler: Awaited = + projectRequire("wrangler"); + const { supportedCompatibilityDate } = wrangler; + + if ( + typeof supportedCompatibilityDate !== "string" || + !/^\d{4}-\d{2}-\d{2}$/.test(supportedCompatibilityDate) + ) { + throw new Error( + "wrangler does not export a valid supportedCompatibilityDate" + ); + } - if (source === "fallback") { + s.stop( + `${brandColor("compatibility date")} ${dim(supportedCompatibilityDate)}` + ); + return supportedCompatibilityDate; + } catch { s.stop( `${brandColor("compatibility date")} ${dim( - ` Could not find workerd date, falling back to ${date}` + `Could not find workerd date, falling back to "${FALLBACK_COMPAT_DATE}"` )}` ); - } else { - s.stop(`${brandColor("compatibility date")} ${dim(date)}`); + return FALLBACK_COMPAT_DATE; } - return date; } /** diff --git a/packages/create-cloudflare/src/wrangler/config.ts b/packages/create-cloudflare/src/wrangler/config.ts index 24a214fc7a..c3af492224 100644 --- a/packages/create-cloudflare/src/wrangler/config.ts +++ b/packages/create-cloudflare/src/wrangler/config.ts @@ -1,6 +1,5 @@ import { existsSync, mkdirSync } from "node:fs"; import { resolve } from "node:path"; -import { isCompatDate } from "@cloudflare/workers-utils"; import { getWorkerdCompatibilityDate } from "helpers/compatDate"; import { readFile, writeFile, writeJSON } from "helpers/files"; import { @@ -14,6 +13,7 @@ import TOML from "smol-toml"; import type { CommentObject, Reviver } from "comment-json"; import type { TomlTable } from "smol-toml"; import type { C3Context } from "types"; +import type { CompatDate } from "wrangler"; /** * Update the `wrangler.(toml|json|jsonc)` file for this project by: @@ -216,11 +216,14 @@ export const addVscodeConfig = (ctx: C3Context) => { async function getCompatibilityDate( tentativeDate: unknown, projectPath: string -): Promise { - if (typeof tentativeDate === "string" && isCompatDate(tentativeDate)) { +): Promise { + if ( + typeof tentativeDate === "string" && + /^\d{4}-\d{2}-\d{2}$/.test(tentativeDate) + ) { // Use the tentative date when it is valid. // It may be there for a specific compat reason - return tentativeDate; + return tentativeDate as CompatDate; } // Fallback to the latest workerd date return getWorkerdCompatibilityDate(projectPath); diff --git a/packages/miniflare/README.md b/packages/miniflare/README.md index f6f2408a5e..77892c01d0 100644 --- a/packages/miniflare/README.md +++ b/packages/miniflare/README.md @@ -933,6 +933,13 @@ defined at the top-level. Returns the same object returned from incoming `Request`'s `cf` property. This object depends on the `cf` property from `SharedOptions`. +### `supportedCompatibilityDate` + +Each workerd release exposes a supported compat date that it guarantees can be used with this version of the package. This date is up to 7 days after the release date of the version. +Normally this `supportedCompatibilityDate` is equal to the value exported by workerd, but if this supported compat date is in the future (because the workerd package was released in the last 7 days) then the value of this export is today. + +This value is a string in the `YYYY-MM-DD` format (e.g. `"2025-09-27"`). + ## Configuration ### Local `workerd` diff --git a/packages/miniflare/src/runtime/compatibility-date.ts b/packages/miniflare/src/runtime/compatibility-date.ts new file mode 100644 index 0000000000..93260e9306 --- /dev/null +++ b/packages/miniflare/src/runtime/compatibility-date.ts @@ -0,0 +1,58 @@ +import assert from "node:assert"; +import { compatibilityDate as workerdCompatibilityDate } from "workerd"; + +export type YYYY = `${number}${number}${number}${number}`; +export type MM = `${number}${number}`; +export type DD = `${number}${number}`; + +/** + * String representing a date following the Cloudflare compatibility date format, such as `2025-09-27` + */ +export type CompatDate = `${YYYY}-${MM}-${DD}`; + +/** + * Discern whether a string represents a compatibility date (`YYYY-MM-DD`) + * + * @param str The target string + * @returns true if the string represents a compatibility date, false otherwise + */ +export function isCompatDate(str: string): str is CompatDate { + return /^\d{4}-\d{2}-\d{2}$/.test(str); +} + +/** + * Returns the date formatted as a compatibility date + * + * @param date The target date to convert + * @returns The date as a CompatDate string (`YYYY-MM-DD`) + */ +export function formatCompatibilityDate(date: Date): CompatDate { + const compatDate = date.toISOString().slice(0, 10); + assert(isCompatDate(compatDate)); + return compatDate; +} + +/** + * Gets a safe compatibility date from workerd. If the workerd compatibility + * date is in the future, returns today's date instead. This handles the case + * where workerd releases set their compatibility date up to 7 days in the future. + * + * @return The compatibility Date (`YYYY-MM-DD`) + */ +function getSafeCompatibilityDate(): CompatDate { + // The compatibility data from workerd follows the CompatDate format + assert(isCompatDate(workerdCompatibilityDate)); + + const today = formatCompatibilityDate(new Date()); + + if (workerdCompatibilityDate > today) { + return today; + } + + return workerdCompatibilityDate; +} + +/** `YYYY-MM-DD` compatibility date */ +const supportedCompatibilityDate = getSafeCompatibilityDate(); + +export { supportedCompatibilityDate }; diff --git a/packages/miniflare/src/runtime/index.ts b/packages/miniflare/src/runtime/index.ts index aad29fc22d..b7ff5dd4ff 100644 --- a/packages/miniflare/src/runtime/index.ts +++ b/packages/miniflare/src/runtime/index.ts @@ -6,9 +6,7 @@ import path from "node:path"; import rl from "node:readline"; import { Readable, Transform } from "node:stream"; import { $ as $colors, red } from "kleur/colors"; -import workerdPath, { - compatibilityDate as workerdCompatibilityDate, -} from "workerd"; +import workerdPath from "workerd"; import { z } from "zod"; import { SERVICE_LOOPBACK, SOCKET_ENTRY } from "../plugins"; import { MiniflareCoreError } from "../shared"; @@ -350,17 +348,12 @@ export class Runtime { export * from "./config"; -/** - * Gets a safe compatibility date from workerd. If the workerd compatibility - * date is in the future, returns today's date instead. This handles the case - * where workerd releases set their compatibility date up to 7 days in the future. - */ -function getSafeCompatibilityDate(): string { - const today = new Date().toISOString().slice(0, 10); - if (workerdCompatibilityDate > today) { - return today; - } - return workerdCompatibilityDate; -} - -export const supportedCompatibilityDate = getSafeCompatibilityDate(); +export { + supportedCompatibilityDate, + formatCompatibilityDate, + isCompatDate, + type CompatDate, + type YYYY, + type MM, + type DD, +} from "./compatibility-date"; diff --git a/packages/miniflare/test/runtime/compatibility-date.spec.ts b/packages/miniflare/test/runtime/compatibility-date.spec.ts new file mode 100644 index 0000000000..2e344b5698 --- /dev/null +++ b/packages/miniflare/test/runtime/compatibility-date.spec.ts @@ -0,0 +1,93 @@ +import { + formatCompatibilityDate, + isCompatDate, + supportedCompatibilityDate, +} from "miniflare"; +import { describe, test } from "vitest"; + +describe("isCompatDate", () => { + test("returns true for valid date strings", ({ expect }) => { + expect(isCompatDate("2024-01-15")).toBe(true); + expect(isCompatDate("2023-12-31")).toBe(true); + expect(isCompatDate("2000-01-01")).toBe(true); + expect(isCompatDate("9999-12-31")).toBe(true); + }); + + test("returns false for invalid date formats", ({ expect }) => { + // Missing leading zeros + expect(isCompatDate("2024-1-15")).toBe(false); + expect(isCompatDate("2024-01-5")).toBe(false); + + // Wrong separators + expect(isCompatDate("2024/01/15")).toBe(false); + expect(isCompatDate("2024.01.15")).toBe(false); + + // Wrong length + expect(isCompatDate("24-01-15")).toBe(false); + expect(isCompatDate("2024-001-15")).toBe(false); + + // Invalid strings + expect(isCompatDate("not-a-date")).toBe(false); + expect(isCompatDate("")).toBe(false); + expect(isCompatDate("2024")).toBe(false); + expect(isCompatDate("2024-01")).toBe(false); + + // Extra characters + expect(isCompatDate("2024-01-15T00:00:00")).toBe(false); + expect(isCompatDate(" 2024-01-15")).toBe(false); + expect(isCompatDate("2024-01-15 ")).toBe(false); + + // Reverse Date + expect(isCompatDate("01-01-2000")).toBe(false); + }); +}); + +describe("formatCompatibilityDate", () => { + test("returns correctly formatted date string", ({ expect }) => { + // Use UTC dates to avoid timezone issues + const date = new Date(Date.UTC(2024, 0, 15)); + expect(formatCompatibilityDate(date)).toBe("2024-01-15"); + }); + + test("pads single-digit months and days with zeros", ({ expect }) => { + const date1 = new Date(Date.UTC(2024, 0, 1)); + expect(formatCompatibilityDate(date1)).toBe("2024-01-01"); + + const date2 = new Date(Date.UTC(2024, 8, 5)); + expect(formatCompatibilityDate(date2)).toBe("2024-09-05"); + }); + + test("handles year boundaries correctly", ({ expect }) => { + const date1 = new Date(Date.UTC(2023, 11, 31)); + expect(formatCompatibilityDate(date1)).toBe("2023-12-31"); + + const date2 = new Date(Date.UTC(2024, 0, 1)); + expect(formatCompatibilityDate(date2)).toBe("2024-01-01"); + }); + + test("handles various years correctly", ({ expect }) => { + const date1 = new Date(Date.UTC(2000, 5, 15)); + expect(formatCompatibilityDate(date1)).toBe("2000-06-15"); + + const date2 = new Date(Date.UTC(2099, 11, 31)); + expect(formatCompatibilityDate(date2)).toBe("2099-12-31"); + }); +}); + +describe("supportedCompatibilityDate", () => { + test("returns a valid compat date string", ({ expect }) => { + expect(isCompatDate(supportedCompatibilityDate)).toBe(true); + }); + + test("should be parseable as a date", ({ expect }) => { + const parsed = new Date(supportedCompatibilityDate); + expect(parsed.toString()).not.toBe("Invalid Date"); + }); + + test("should not be in the future", ({ expect }) => { + // supportedCompatibilityDate is already a YYYY-MM-DD string, which sorts + // lexicographically as a date. Compare directly against today's date. + const todayDate = formatCompatibilityDate(new Date()); + expect(supportedCompatibilityDate <= todayDate).toBe(true); + }); +}); diff --git a/packages/vite-plugin-cloudflare/src/index.ts b/packages/vite-plugin-cloudflare/src/index.ts index 515f3a963b..99ae64f1db 100644 --- a/packages/vite-plugin-cloudflare/src/index.ts +++ b/packages/vite-plugin-cloudflare/src/index.ts @@ -1,3 +1,4 @@ +import { supportedCompatibilityDate } from "miniflare"; import { assertWranglerVersion } from "./assert-wrangler-version"; import { PluginContext } from "./context"; import { resolvePluginConfig } from "./plugin-config"; @@ -23,9 +24,30 @@ import { wasmHelperPlugin } from "./plugins/wasm"; import { debuglog } from "./utils"; import type { SharedContext } from "./context"; import type { PluginConfig } from "./plugin-config"; +import type { CompatDate } from "miniflare"; import type * as vite from "vite"; -export { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; +// TODO: simplify this function in the next major release (DEVX-2533) +/** + * Gets the compatibility date from the local workerd version. + * + * Note: the function's signature is as is because it needs to be backward compatibly with + * a previous iteration of this, it will be simplified in the next major version of this package. + * + * @param _options Unused argument (present only for backward compatibility) + * @returns Object containing the compatibility date (this is not the date directly for backward compatibility) + */ +export function getLocalWorkerdCompatibilityDate(_options?: { + projectPath?: string; +}): { + date: CompatDate; + source: "workerd"; +} { + return { + date: supportedCompatibilityDate, + source: "workerd", + }; +} export type { PluginConfig } from "./plugin-config"; export type { WorkerConfig } from "./workers-configs"; diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index e86e928e06..8400c560ed 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -1,7 +1,7 @@ import * as path from "node:path"; import { parseStaticRouting } from "@cloudflare/workers-shared/utils/configuration/parseStaticRouting"; -import { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; import { defu } from "defu"; +import { supportedCompatibilityDate } from "miniflare"; import * as vite from "vite"; import * as wrangler from "wrangler"; import { getWorkerConfigs } from "./deploy-config"; @@ -244,11 +244,7 @@ function resolveWorkerConfig( configCustomizer: options.configCustomizer, }); - const { date } = getLocalWorkerdCompatibilityDate({ - projectPath: options.root, - }); - - workerConfig.compatibility_date ??= date; + workerConfig.compatibility_date ??= supportedCompatibilityDate; if (isEntryWorker) { workerConfig.name ??= wrangler.unstable_getWorkerNameFromProject( diff --git a/packages/workers-utils/src/compatibility-date.ts b/packages/workers-utils/src/compatibility-date.ts deleted file mode 100644 index a025a8f29b..0000000000 --- a/packages/workers-utils/src/compatibility-date.ts +++ /dev/null @@ -1,102 +0,0 @@ -import assert from "node:assert"; -import module from "node:module"; -import path from "node:path"; - -type YYYY = `${number}${number}${number}${number}`; -type MM = `${number}${number}`; -type DD = `${number}${number}`; - -/** - * Represents a valid compatibility date, a string such as `2025-09-27` - */ -export type CompatDate = `${YYYY}-${MM}-${DD}`; - -type GetCompatDateOptions = { - projectPath?: string; -}; - -type GetCompatDateResult = { - date: CompatDate; - source: "workerd" | "fallback"; -}; - -/** - * Gets the compatibility date of the locally installed workerd package. - * - * If the package is not found the fallback date of 2025-09-27 is returned instead. - * - * Additionally, if the workerd date is set to the future then the current date is returned instead. - * - * @param options.projectPath the path to the project - * @returns an object including the compatibility date and its source - */ -export function getLocalWorkerdCompatibilityDate({ - projectPath = process.cwd(), -}: GetCompatDateOptions = {}): GetCompatDateResult { - try { - // Note: createRequire expects a filename, not a directory. When given a directory, - // Node.js looks for node_modules in the parent directory instead of the given directory. - // Appending package.json ensures resolution starts from the correct location. - const projectRequire = module.createRequire( - path.join(projectPath, "package.json") - ); - const miniflareEntry = projectRequire.resolve("miniflare"); - const miniflareRequire = module.createRequire(miniflareEntry); - const miniflareWorkerd = miniflareRequire("workerd") as { - compatibilityDate: string; - }; - const workerdDate = miniflareWorkerd.compatibilityDate; - return { - date: toSafeCompatibilityDate(new Date(workerdDate)), - source: "workerd", - }; - } catch {} - - return { - date: "2025-09-27", - source: "fallback", - }; -} - -/** - * Workerd releases often have a date for the following day. - * Unfortunately, Workers deployments will fail if they specify a compatibility date in the future. This means that most - * who create a new project on the same day as a workerd release will have their deployments fail until they - * manually adjust the compatibility date. - * - * To work around this, we must manually ensure that the compat date is not on a future UTC day when there was a recent workerd release. - * - * This function is the used to convert potential future dates to safe compatibility dates. - * - * @param date The local workerd date to check and convert - * @returns A compat date created using today's date if the local workerd date is in the future, one using the local workerd date otherwise - */ -function toSafeCompatibilityDate(date: Date): CompatDate { - if (date.getTime() > Date.now()) { - return formatCompatibilityDate(new Date()); - } - - return formatCompatibilityDate(date); -} - -/** - * Discern whether a string represents a compatibility date (`YYYY-MM-DD`) - * - * @param str The target string - * @returns true if the string represents a compatibility date, false otherwise - */ -export function isCompatDate(str: string): str is CompatDate { - return /^\d{4}-\d{2}-\d{2}$/.test(str); -} - -/** - * Returns the date formatted as a compatibility date - * - * @param date The target date to convert - * @returns The date as a CompatDate string (a string following the format `YYYY-MM-DD`) - */ -export function formatCompatibilityDate(date: Date): CompatDate { - const compatDate = date.toISOString().slice(0, 10); - assert(isCompatDate(compatDate)); - return compatDate; -} diff --git a/packages/workers-utils/src/construct-wrangler-config.ts b/packages/workers-utils/src/construct-wrangler-config.ts index 1fc29d267a..0c824cf104 100644 --- a/packages/workers-utils/src/construct-wrangler-config.ts +++ b/packages/workers-utils/src/construct-wrangler-config.ts @@ -1,4 +1,3 @@ -import { formatCompatibilityDate } from "./compatibility-date"; import { ENVIRONMENT_TAG_PREFIX, SERVICE_TAG_PREFIX } from "./constants"; import { mapWorkerMetadataBindings } from "./map-worker-metadata-bindings"; import type { RawConfig } from "./config"; @@ -79,7 +78,7 @@ function convertWorkerToWranglerConfig(config: APIWorkerConfig): RawConfig { workers_dev: config.subdomain.enabled, preview_urls: config.subdomain.previews_enabled, compatibility_date: - config.compatibility_date ?? formatCompatibilityDate(new Date()), + config.compatibility_date ?? new Date().toISOString().slice(0, 10), compatibility_flags: config.compatibility_flags, ...(allRoutes.length ? { routes: allRoutes } : {}), placement: diff --git a/packages/workers-utils/src/index.ts b/packages/workers-utils/src/index.ts index 939387290e..a120ca25e9 100644 --- a/packages/workers-utils/src/index.ts +++ b/packages/workers-utils/src/index.ts @@ -85,13 +85,6 @@ export * from "./environment-variables/misc-variables"; export { getGlobalWranglerConfigPath } from "./global-wrangler-config-path"; -export { - getLocalWorkerdCompatibilityDate, - formatCompatibilityDate, - isCompatDate, -} from "./compatibility-date"; -export type { CompatDate } from "./compatibility-date"; - export { isDockerfile } from "./config/validation"; export { isDirectory, removeDir, removeDirSync } from "./fs-helpers"; diff --git a/packages/workers-utils/tests/compatibility-date.test.ts b/packages/workers-utils/tests/compatibility-date.test.ts deleted file mode 100644 index f258df9866..0000000000 --- a/packages/workers-utils/tests/compatibility-date.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import module from "node:module"; -import path from "node:path"; -import { beforeEach, describe, it, vi } from "vitest"; -import { getLocalWorkerdCompatibilityDate } from "../src/compatibility-date"; - -describe("getLocalWorkerdCompatibilityDate", () => { - beforeEach(() => { - vi.setSystemTime(vi.getRealSystemTime()); - }); - - it("should successfully get the local latest compatibility date from the local workerd instance", ({ - expect, - }) => { - const createRequireSpy = vi - .spyOn(module, "createRequire") - .mockImplementation(() => { - const mockedRequire = ((pkg: string) => { - if (pkg === "workerd") { - return { compatibilityDate: "2025-01-10" }; - } - return {}; - }) as NodeJS.Require; - mockedRequire.resolve = (() => "") as unknown as NodeJS.RequireResolve; - return mockedRequire; - }); - const { date, source } = getLocalWorkerdCompatibilityDate({ - projectPath: "/test/project", - }); - expect(date).toBe("2025-01-10"); - expect(source).toEqual("workerd"); - // Verify createRequire is called with a file path (package.json), not just a directory - expect(createRequireSpy).toHaveBeenCalledWith( - path.join("/test/project", "package.json") - ); - }); - - it("should fallback to the fallback date if it fails to get the date from a local workerd instance", ({ - expect, - }) => { - const createRequireSpy = vi - .spyOn(module, "createRequire") - .mockImplementation( - // This breaks the require function that createRequire generate, causing us not to find - // the local miniflare/workerd instance - () => ({}) as NodeJS.Require - ); - const { date, source } = getLocalWorkerdCompatibilityDate({ - projectPath: "/test/project", - }); - const fallbackCompatDate = "2025-09-27"; - expect(date).toEqual(fallbackCompatDate); - expect(source).toEqual("fallback"); - // Verify createRequire is called with a file path even when resolution fails - expect(createRequireSpy).toHaveBeenCalledWith( - path.join("/test/project", "package.json") - ); - }); - - it("should use today's date if the local workerd's date is in the future", async ({ - expect, - }) => { - vi.setSystemTime("2025-01-09T23:59:59.999Z"); - vi.spyOn(module, "createRequire").mockImplementation(() => { - const mockedRequire = ((pkg: string) => { - if (pkg === "workerd") { - return { compatibilityDate: "2025-01-10" }; - } - return {}; - }) as NodeJS.Require; - mockedRequire.resolve = (() => "") as unknown as NodeJS.RequireResolve; - return mockedRequire; - }); - const { date, source } = getLocalWorkerdCompatibilityDate({ - projectPath: "/test/project", - }); - const todaysDate = "2025-01-09"; - expect(date).toEqual(todaysDate); - expect(source).toEqual("workerd"); - }); -}); diff --git a/packages/wrangler/e2e/pages-dev.test.ts b/packages/wrangler/e2e/pages-dev.test.ts index 1d5989edbc..b6ce8548d4 100644 --- a/packages/wrangler/e2e/pages-dev.test.ts +++ b/packages/wrangler/e2e/pages-dev.test.ts @@ -1,7 +1,7 @@ import { existsSync } from "node:fs"; import path from "node:path"; -import { formatCompatibilityDate } from "@cloudflare/workers-utils"; import getPort from "get-port"; +import { formatCompatibilityDate } from "miniflare"; import dedent from "ts-dedent"; import { fetch } from "undici"; import { describe, it } from "vitest"; diff --git a/packages/wrangler/src/__tests__/autoconfig/run.test.ts b/packages/wrangler/src/__tests__/autoconfig/run.test.ts index 3916a1bf90..35e30450bd 100644 --- a/packages/wrangler/src/__tests__/autoconfig/run.test.ts +++ b/packages/wrangler/src/__tests__/autoconfig/run.test.ts @@ -26,16 +26,13 @@ import { writeWorkerSource } from "../helpers/write-worker-source"; import type { Framework } from "../../autoconfig/frameworks"; import type { MockInstance } from "vitest"; -vi.mock("@cloudflare/workers-utils", async (importOriginal) => { +vi.mock("miniflare", async (importOriginal) => { const originalModule = // eslint-disable-next-line @typescript-eslint/consistent-type-imports - await importOriginal>(); + await importOriginal>(); return { ...originalModule, - getLocalWorkerdCompatibilityDate: vi.fn(() => ({ - date: "2000-01-01", - source: "workerd", - })), + supportedCompatibilityDate: "2000-01-01", }; }); diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index 802b303021..33c0f982a0 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -2,7 +2,6 @@ import * as fs from "node:fs"; import { COMPLIANCE_REGION_CONFIG_UNKNOWN, FatalError, - getLocalWorkerdCompatibilityDate, } from "@cloudflare/workers-utils"; import { writeWranglerConfig } from "@cloudflare/workers-utils/test-helpers"; import ci from "ci-info"; @@ -19,6 +18,7 @@ import { it, vi, } from "vitest"; +import { supportedCompatibilityDate } from "../api"; /* eslint-enable no-restricted-imports */ import { ConfigController } from "../api/startDevWorker/ConfigController"; import { unwrapHook } from "../api/startDevWorker/utils"; @@ -278,11 +278,7 @@ describe.sequential("wrangler dev", () => { fs.writeFileSync("index.js", `export default {};`); await runWranglerUntilConfig("dev"); - // Use getLocalWorkerdCompatibilityDate() which applies the same safe date - // conversion as wrangler does (converting future dates to today's date) - const { date: currentDate } = getLocalWorkerdCompatibilityDate(); - - expect(std.warn.replaceAll(currentDate, "")) + expect(std.warn.replaceAll(supportedCompatibilityDate, "")) .toMatchInlineSnapshot(` "▲ [WARNING] No compatibility_date was specified. Using the installed Workers runtime's latest supported date: . diff --git a/packages/wrangler/src/api/index.ts b/packages/wrangler/src/api/index.ts index d0f0937f5d..5da40c0426 100644 --- a/packages/wrangler/src/api/index.ts +++ b/packages/wrangler/src/api/index.ts @@ -71,8 +71,11 @@ export type { PlatformProxy, SourcelessWorkerOptions, Unstable_MiniflareWorkerOptions, + CompatDate, } from "./integrations"; +export { supportedCompatibilityDate, isCompatDate } from "miniflare"; + // Exports from ./remoteBindings export { startRemoteProxySession, diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index 3f3f0cc617..88701f7b70 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -1,10 +1,10 @@ import { resolveDockerHost } from "@cloudflare/containers-shared"; +import { getDockerPath, getRegistryPath } from "@cloudflare/workers-utils"; import { - getDockerPath, - getLocalWorkerdCompatibilityDate, - getRegistryPath, -} from "@cloudflare/workers-utils"; -import { kCurrentWorker, Miniflare } from "miniflare"; + kCurrentWorker, + Miniflare, + supportedCompatibilityDate, +} from "miniflare"; import { getAssetsOptions, NonExistentAssetsDirError } from "../../../assets"; import { readConfig } from "../../../config"; import { partitionDurableObjectBindings } from "../../../deployment-bundle/entry"; @@ -46,14 +46,23 @@ export { readConfig as unstable_readConfig }; export { getDurableObjectClassNameToUseSQLiteMap as unstable_getDurableObjectClassNameToUseSQLiteMap }; /** - * @deprecated use `getLocalWorkerdCompatibilityDate` from "@cloudflare/workers-utils" instead. + * @deprecated use `supportedCompatibilityDate` instead. * * We're keeping this function only not to break the vite plugin that relies on it, we should remove it as soon as possible. + * + * @returns YYYY-MM-DD compatibility date */ export function unstable_getDevCompatibilityDate() { - return getLocalWorkerdCompatibilityDate().date; + return supportedCompatibilityDate; } +export { + supportedCompatibilityDate, + isCompatDate, + formatCompatibilityDate, +} from "miniflare"; +export type { CompatDate } from "miniflare"; + export { getWorkerNameFromProject as unstable_getWorkerNameFromProject } from "../../../autoconfig/details"; export type { Config as Unstable_Config, diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index e200ef3583..6236b916c5 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -5,11 +5,10 @@ import { configFileName, getDisableConfigWatching, getDockerPath, - getLocalWorkerdCompatibilityDate, UserError, } from "@cloudflare/workers-utils"; import { watch } from "chokidar"; -import { getWorkerRegistry } from "miniflare"; +import { getWorkerRegistry, supportedCompatibilityDate } from "miniflare"; import { getAssetsOptions, validateAssetsArgsAndConfig } from "../../assets"; import { fillOpenAPIConfiguration } from "../../cloudchamber/common"; import { readConfig } from "../../config"; @@ -314,11 +313,7 @@ async function resolveConfig( previousName ?? crypto.randomUUID(), config: config.configPath, - compatibilityDate: getDevCompatibilityDate( - entry.projectRoot, - config, - input.compatibilityDate - ), + compatibilityDate: getDevCompatibilityDate(config, input.compatibilityDate), compatibilityFlags: input.compatibilityFlags ?? config.compatibility_flags, complianceRegion: input.complianceRegion ?? config.compliance_region, pythonModules: { @@ -473,13 +468,10 @@ async function resolveConfig( * @returns the compatibility date to use in development */ function getDevCompatibilityDate( - projectPath: string, config: Config | undefined, compatibilityDate = config?.compatibility_date ): string { - const { date: workerdDate } = getLocalWorkerdCompatibilityDate({ - projectPath, - }); + const workerdDate = supportedCompatibilityDate; if (config?.configPath && compatibilityDate === undefined) { logger.warn( diff --git a/packages/wrangler/src/autoconfig/frameworks/analog.ts b/packages/wrangler/src/autoconfig/frameworks/analog.ts index 28ae2a3889..ddedd8a450 100644 --- a/packages/wrangler/src/autoconfig/frameworks/analog.ts +++ b/packages/wrangler/src/autoconfig/frameworks/analog.ts @@ -3,7 +3,7 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; import { updateStatus } from "@cloudflare/cli"; import { blue } from "@cloudflare/cli/colors"; -import { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; +import { supportedCompatibilityDate } from "miniflare"; import * as recast from "recast"; import semiver from "semiver"; import { mergeObjectProperties, transformFile } from "../c3-vendor/codemod"; @@ -48,10 +48,6 @@ async function updateViteConfig(projectPath: string) { throw new Error("Could not find Vite config file to modify"); } - const { date: compatDate } = getLocalWorkerdCompatibilityDate({ - projectPath, - }); - updateStatus(`Updating configuration in ${blue(viteConfigPath)}`); transformFile(viteConfigPath, { @@ -73,7 +69,7 @@ async function updateViteConfig(projectPath: string) { ), b.objectProperty( b.identifier("compatibilityDate"), - b.stringLiteral(compatDate) + b.stringLiteral(supportedCompatibilityDate) ), ]) ), diff --git a/packages/wrangler/src/autoconfig/frameworks/solid-start.ts b/packages/wrangler/src/autoconfig/frameworks/solid-start.ts index ff777b0f29..6b9f18692a 100644 --- a/packages/wrangler/src/autoconfig/frameworks/solid-start.ts +++ b/packages/wrangler/src/autoconfig/frameworks/solid-start.ts @@ -1,7 +1,7 @@ import assert from "node:assert"; import { updateStatus } from "@cloudflare/cli"; import { blue } from "@cloudflare/cli/colors"; -import { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; +import { supportedCompatibilityDate } from "miniflare"; import * as recast from "recast"; import semiver from "semiver"; import { mergeObjectProperties, transformFile } from "../c3-vendor/codemod"; @@ -88,10 +88,6 @@ function updateViteConfigFile(projectPath: string): void { function updateAppConfigFile(projectPath: string): void { const filePath = `app.config.${usesTypescript(projectPath) ? "ts" : "js"}`; - const { date: compatDate } = getLocalWorkerdCompatibilityDate({ - projectPath, - }); - updateStatus(`Updating configuration in ${blue(filePath)}`); transformFile(filePath, { @@ -115,7 +111,7 @@ function updateAppConfigFile(projectPath: string): void { ), b.objectProperty( b.identifier("compatibilityDate"), - b.stringLiteral(compatDate) + b.stringLiteral(supportedCompatibilityDate) ), ]) ), diff --git a/packages/wrangler/src/autoconfig/run.ts b/packages/wrangler/src/autoconfig/run.ts index be68fbfb1d..2e8c212cee 100644 --- a/packages/wrangler/src/autoconfig/run.ts +++ b/packages/wrangler/src/autoconfig/run.ts @@ -2,11 +2,8 @@ import assert from "node:assert"; import { existsSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { - FatalError, - getLocalWorkerdCompatibilityDate, - parseJSONC, -} from "@cloudflare/workers-utils"; +import { FatalError, parseJSONC } from "@cloudflare/workers-utils"; +import { supportedCompatibilityDate } from "miniflare"; import { runCommand } from "../deployment-bundle/run-custom-build"; import { confirm } from "../dialogs"; import { logger } from "../logger"; @@ -88,14 +85,10 @@ export async function runAutoConfig( "The Output Directory is unexpectedly missing" ); - const { date: compatibilityDate } = getLocalWorkerdCompatibilityDate({ - projectPath: autoConfigDetails.projectPath, - }); - const wranglerConfig: RawConfig = { $schema: "node_modules/wrangler/config-schema.json", name: autoConfigDetails.workerName, - compatibility_date: compatibilityDate, + compatibility_date: supportedCompatibilityDate, observability: { enabled: true, }, diff --git a/packages/wrangler/src/cli.ts b/packages/wrangler/src/cli.ts index bfb4c98185..4d49a518cc 100644 --- a/packages/wrangler/src/cli.ts +++ b/packages/wrangler/src/cli.ts @@ -85,6 +85,9 @@ export type { Unstable_MiniflareWorkerOptions, }; +export { supportedCompatibilityDate, isCompatDate } from "./api"; +export type { CompatDate } from "./api"; + export { printBindings as unstable_printBindings } from "./utils/print-bindings"; // Export internal APIs required by the Vitest integration as `unstable_` diff --git a/packages/wrangler/src/deploy/deploy.ts b/packages/wrangler/src/deploy/deploy.ts index 15885a7b61..ff8d3a5ebe 100644 --- a/packages/wrangler/src/deploy/deploy.ts +++ b/packages/wrangler/src/deploy/deploy.ts @@ -8,12 +8,12 @@ import { APIError, configFileName, experimental_patchConfig, - formatCompatibilityDate, formatConfigSnippet, getDockerPath, parseNonHyphenedUuid, UserError, } from "@cloudflare/workers-utils"; +import { formatCompatibilityDate } from "miniflare"; import PQueue from "p-queue"; import { Response } from "undici"; import { syncAssets } from "../assets"; diff --git a/packages/wrangler/src/deploy/index.ts b/packages/wrangler/src/deploy/index.ts index c1cc519cc6..67c12da5b4 100644 --- a/packages/wrangler/src/deploy/index.ts +++ b/packages/wrangler/src/deploy/index.ts @@ -3,11 +3,11 @@ import { statSync, writeFileSync } from "node:fs"; import path from "node:path"; import { configFileName, - formatCompatibilityDate, getCIOverrideName, UserError, } from "@cloudflare/workers-utils"; import chalk from "chalk"; +import { formatCompatibilityDate } from "miniflare"; import { getAssetsOptions, validateAssetsArgsAndConfig } from "../assets"; import { getDetailsForAutoConfig } from "../autoconfig/details"; import { runAutoConfig } from "../autoconfig/run"; diff --git a/packages/wrangler/src/deployment-bundle/entry.ts b/packages/wrangler/src/deployment-bundle/entry.ts index 3e34ef0551..89a430d809 100644 --- a/packages/wrangler/src/deployment-bundle/entry.ts +++ b/packages/wrangler/src/deployment-bundle/entry.ts @@ -1,10 +1,10 @@ import path from "node:path"; import { configFileName, - formatCompatibilityDate, formatConfigSnippet, UserError, } from "@cloudflare/workers-utils"; +import { formatCompatibilityDate } from "miniflare"; import dedent from "ts-dedent"; import { sniffUserAgent } from "../package-manager"; import { guessWorkerFormat } from "./guess-worker-format"; diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 4c958cfa40..11cee94b26 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -6,11 +6,11 @@ import { setTimeout } from "node:timers/promises"; import { configFileName, FatalError, - formatCompatibilityDate, UserError, } from "@cloudflare/workers-utils"; import { watch } from "chokidar"; import * as esbuild from "esbuild"; +import { formatCompatibilityDate } from "miniflare"; import { readConfig } from "../config"; import { getConfigCache } from "../config-cache"; import { createCommand } from "../core/create-command"; diff --git a/packages/wrangler/src/pages/download-config.ts b/packages/wrangler/src/pages/download-config.ts index 25ad2c9df0..4ca2ad7ec3 100644 --- a/packages/wrangler/src/pages/download-config.ts +++ b/packages/wrangler/src/pages/download-config.ts @@ -3,10 +3,9 @@ import { writeFile } from "node:fs/promises"; import { COMPLIANCE_REGION_CONFIG_PUBLIC, FatalError, - formatCompatibilityDate, } from "@cloudflare/workers-utils"; import chalk from "chalk"; -import { supportedCompatibilityDate } from "miniflare"; +import { formatCompatibilityDate, supportedCompatibilityDate } from "miniflare"; import TOML from "smol-toml"; import { fetchResult } from "../cfetch"; import { getConfigCache } from "../config-cache"; diff --git a/packages/wrangler/src/versions/upload.ts b/packages/wrangler/src/versions/upload.ts index a08416f3d6..69a79db05b 100644 --- a/packages/wrangler/src/versions/upload.ts +++ b/packages/wrangler/src/versions/upload.ts @@ -6,7 +6,6 @@ import path from "node:path"; import { blue, gray } from "@cloudflare/cli/colors"; import { configFileName, - formatCompatibilityDate, formatConfigSnippet, getCIGeneratePreviewAlias, getCIOverrideName, @@ -14,6 +13,7 @@ import { ParseError, UserError, } from "@cloudflare/workers-utils"; +import { formatCompatibilityDate } from "miniflare"; import { Response } from "undici"; import { getAssetsOptions,