diff --git a/.changeset/episcopature-theocrasy-matter.md b/.changeset/episcopature-theocrasy-matter.md new file mode 100644 index 000000000000..e736578e8ea3 --- /dev/null +++ b/.changeset/episcopature-theocrasy-matter.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/workers-playground": minor +--- + +Add expandable object logging and improved console UI + +The Quick Editor console now displays logged objects and arrays as expandable tree views instead of `[object Object]`. diff --git a/.changeset/native-readline-module.md b/.changeset/native-readline-module.md new file mode 100644 index 000000000000..a629d1f16f8e --- /dev/null +++ b/.changeset/native-readline-module.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/unenv-preset": minor +--- + +Add support for native `node:readline` module when the `enable_nodejs_readline_module` compatibility flag is enabled. + +This feature is currently experimental and requires both the `enable_nodejs_readline_module` and `experimental` compatibility flags to be set. diff --git a/packages/unenv-preset/package.json b/packages/unenv-preset/package.json index 49b8fd91cdf7..8aae049662bd 100644 --- a/packages/unenv-preset/package.json +++ b/packages/unenv-preset/package.json @@ -49,7 +49,7 @@ }, "peerDependencies": { "unenv": "2.0.0-rc.24", - "workerd": "^1.20260214.0" + "workerd": "^1.20260218.0" }, "peerDependenciesMeta": { "workerd": { diff --git a/packages/unenv-preset/src/preset.ts b/packages/unenv-preset/src/preset.ts index a3ca8ed18e05..4d484d682a95 100644 --- a/packages/unenv-preset/src/preset.ts +++ b/packages/unenv-preset/src/preset.ts @@ -84,6 +84,7 @@ export function getCloudflarePreset({ const ttyOverrides = getTtyOverrides(compat); const childProcessOverrides = getChildProcessOverrides(compat); const workerThreadsOverrides = getWorkerThreadsOverrides(compat); + const readlineOverrides = getReadlineOverrides(compat); // "dynamic" as they depend on the compatibility date and flags const dynamicNativeModules = [ @@ -109,6 +110,7 @@ export function getCloudflarePreset({ ...ttyOverrides.nativeModules, ...childProcessOverrides.nativeModules, ...workerThreadsOverrides.nativeModules, + ...readlineOverrides.nativeModules, ]; // "dynamic" as they depend on the compatibility date and flags @@ -134,6 +136,7 @@ export function getCloudflarePreset({ ...ttyOverrides.hybridModules, ...childProcessOverrides.hybridModules, ...workerThreadsOverrides.hybridModules, + ...readlineOverrides.hybridModules, ]; return { @@ -1057,3 +1060,39 @@ function getWorkerThreadsOverrides({ hybridModules: [], }; } + +/** + * Returns the overrides for `node:readline` and `node:readline/promises` (unenv or workerd) + * + * The native readline implementation: + * - is experimental and has no default enable date + * - can be enabled with the "enable_nodejs_readline_module" flag + * - can be disabled with the "disable_nodejs_readline_module" flag + */ +function getReadlineOverrides({ + compatibilityFlags, +}: { + compatibilityDate: string; + compatibilityFlags: string[]; +}): { nativeModules: string[]; hybridModules: string[] } { + const disabledByFlag = compatibilityFlags.includes( + "disable_nodejs_readline_module" + ); + + const enabledByFlag = + compatibilityFlags.includes("enable_nodejs_readline_module") && + compatibilityFlags.includes("experimental"); + + const enabled = enabledByFlag && !disabledByFlag; + + // When enabled, use the native `readline` and `readline/promises` modules from workerd + return enabled + ? { + nativeModules: ["readline", "readline/promises"], + hybridModules: [], + } + : { + nativeModules: [], + hybridModules: [], + }; +} diff --git a/packages/workers-playground/src/QuickEditor/DevtoolsIframe.tsx b/packages/workers-playground/src/QuickEditor/DevtoolsIframe.tsx index f57472eb5dcb..972eb609bd93 100644 --- a/packages/workers-playground/src/QuickEditor/DevtoolsIframe.tsx +++ b/packages/workers-playground/src/QuickEditor/DevtoolsIframe.tsx @@ -1,37 +1,17 @@ +import { Icon } from "@cloudflare/component-icon"; import { Loading } from "@cloudflare/component-loading"; import { Div, Span } from "@cloudflare/elements"; import { isDarkMode, theme } from "@cloudflare/style-const"; -import { createStyledComponent } from "@cloudflare/style-container"; -import { useContext, useState } from "react"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; import useWebSocket from "react-use-websocket"; +import { ExpandableLogMessage, messageSummary } from "./ExpandableLogMessage"; import FrameErrorBoundary from "./FrameErrorBoundary"; import InvocationIcon from "./InvocationIcon"; import { ServiceContext } from "./QuickEditor"; import type React from "react"; -export const ErrorBar = createStyledComponent( - ({ isDark }) => ({ - width: "4px", - minWidth: "3px", - height: "19px", - backgroundColor: isDark ? "#E81403" : "#F42500", - borderRadius: "20px", - }), - Div -); - -export const SuccessBar = createStyledComponent( - () => ({ - width: "4px", - minWidth: "3px", - height: "19px", - backgroundColor: theme.colors.blue[5], - borderRadius: "15px", - }), - Div -); - type TailEvent = { + id: string; eventTimestamp: number; logs: { message: unknown[]; @@ -136,94 +116,88 @@ type TailEvent = { const TailRow = ({ event }: { event: TailEvent }) => { const isDark = isDarkMode(); + const requestTimestamp = event.eventTimestamp; return ( <> {[
-
- - - - - {event.event.request?.method} + + + + + {requestTimestamp && ( - {event.event.request?.url} + {new Date(requestTimestamp).toISOString().slice(11, 19)} + )} + {event.event.request?.method} + + {event.event.request?.url} -
+
, - ...event.logs.map((log, idx) => ( + ...event.logs.map((log, logIdx) => (
-
- -
- {log.level === "error" ? ( - - ) : ( - - )} - {/* */} -
- - {log.message.join(" ")} - + + + -
+
)), ]} @@ -231,53 +205,253 @@ const TailRow = ({ event }: { event: TailEvent }) => { ); }; -export function DevtoolsIframe({ url }: { url: string }) { - const [messageHistory, setMessageHistory] = useState([]); +// Up to 3 retries with exponential backoff (1s, 2s, 4s) before giving up. +const MAX_RETRIES = 3; +const RETRY_BASE_DELAY = 1000; + +/** + * Manages the WebSocket connection and notifies parent of errors and new data. + * Remounted via key changes on retry — but log data is owned by the parent. + */ +function TailLogsConnector({ + url, + onError, + onConnected, + onData, +}: { + url: string; + onError: () => void; + onConnected: () => void; + onData: (event: TailEvent) => void; +}) { + const errorNotified = useRef(false); useWebSocket(url, { protocols: "trace-v1", disableJson: true, + onOpen() { + errorNotified.current = false; + onConnected(); + }, async onMessage(event) { - const messageEvent = JSON.parse(await event.data.text()); - - const idx = messageHistory.findIndex((el) => { - return el.eventTimestamp < messageEvent.eventTimestamp; - }); - - messageHistory.splice(idx, 0, messageEvent); - setMessageHistory([...messageHistory]); + const messageEvent = JSON.parse(await event.data.text()) as TailEvent; + onData(messageEvent); + }, + onError() { + if (!errorNotified.current) { + errorNotified.current = true; + onError(); + } + }, + // The tail WebSocket is sometimes unexpectedly closed by the server, which means we need to try and reconnect + onClose() { + if (!errorNotified.current) { + errorNotified.current = true; + onError(); + } }, }); - return messageHistory.length ? ( -
- {messageHistory.map((event, idx) => { - return ; - })} -
- ) : ( -
- Send a request to your Worker to view logs + return null; +} + +/** + * Owns the log data and renders the console UI. + * Uses TailLogsConnector for the WebSocket connection, with auto-retry on errors. + * Log data persists across reconnections. + */ +export function DevtoolsIframe({ url }: { url: string }) { + const [logs, setLogs] = useState([]); + const [retryKey, setRetryKey] = useState(0); + const [retryCount, setRetryCount] = useState(0); + const [retrying, setRetrying] = useState(false); + const timerRef = useRef>(); + const mountedRef = useRef(true); + + // Cleanup on unmount + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }; + }, []); + + // Reset retry state when URL changes (e.g. new preview deployment). + // This is React's documented "reset state on prop change" pattern. + /* eslint-disable react-hooks/set-state-in-effect -- intentional reset on prop change */ + useEffect(() => { + setRetryCount(0); + setRetrying(false); + if (timerRef.current) { + clearTimeout(timerRef.current); + } + }, [url]); + /* eslint-enable react-hooks/set-state-in-effect */ + + const handleConnected = useCallback(() => { + setRetryCount(0); + }, []); + + const handleError = useCallback(() => { + if (retryCount < MAX_RETRIES) { + setRetrying(true); + const delay = RETRY_BASE_DELAY * Math.pow(2, retryCount); + timerRef.current = setTimeout(() => { + if (!mountedRef.current) { + return; + } + setRetryCount((c) => c + 1); + setRetryKey((k) => k + 1); + setRetrying(false); + }, delay); + } else { + // All retries exhausted, increment to trigger exhaustedRetries + setRetryCount((c) => c + 1); + } + }, [retryCount]); + + const handleData = useCallback((event: TailEvent) => { + setLogs((prev) => { + const idx = prev.findIndex( + (el) => el.eventTimestamp < event.eventTimestamp + ); + const next = [...prev]; + if (idx === -1) { + next.push(event); + } else { + next.splice(idx, 0, event); + } + return next; + }); + }, []); + + const isDark = isDarkMode(); + const hasLogs = logs.length > 0; + const exhaustedRetries = retryCount > MAX_RETRIES; + + return ( +
+ {/* WebSocket connector — remounted on retry */} + {!retrying && !exhaustedRetries && ( + + )} + + {/* Header bar */} +
+ + Console + {retrying && ( + + — reconnecting… + + )} + {exhaustedRetries && ( + + — disconnected{" "} + { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + setRetryCount(0); + setRetrying(false); + setRetryKey((k) => k + 1); + }} + > + retry + + + )} + + {hasLogs && ( + setLogs([])} + display="flex" + alignItems="center" + gap="4px" + py="6px" + style={{ userSelect: "none" }} + > + + Clear + + )} +
+ + {/* Content */} + {hasLogs ? ( +
+
+ {logs.map((event) => ( + + ))} +
+
+ ) : ( +
+ + {url ? "Waiting for logs…" : "Connecting…"} + + + Send a request to see console output + +
+ )}
); } + const DevtoolsIframeWithErrorHandling: React.FC = () => { const draftWorker = useContext(ServiceContext); if (!draftWorker.devtoolsUrl) { @@ -289,7 +463,9 @@ const DevtoolsIframeWithErrorHandling: React.FC = () => { height="100%" display="flex" gap={2} - backgroundColor={isDarkMode() ? "#313131" : "white"} + backgroundColor={ + isDarkMode() ? theme.colors.gray[1] : theme.colors.white + } justifyContent={"center"} alignItems={"center"} > diff --git a/packages/workers-playground/src/QuickEditor/ExpandableLogMessage.tsx b/packages/workers-playground/src/QuickEditor/ExpandableLogMessage.tsx new file mode 100644 index 000000000000..7d7ab65b0b81 --- /dev/null +++ b/packages/workers-playground/src/QuickEditor/ExpandableLogMessage.tsx @@ -0,0 +1,284 @@ +import { isDarkMode } from "@cloudflare/style-const"; +import { useState } from "react"; + +/** Type guard for non-null objects (including arrays). */ +function isObject(value: unknown): value is object { + return value !== null && typeof value === "object"; +} + +/** + * Color values for different value types, adapting to dark/light mode. + */ +function typeColors() { + const dark = isDarkMode(); + return { + string: dark ? "#c4b5fd" : "#7c3aed", + number: dark ? "#fbbf24" : "#ea580c", + boolean: dark ? "#f472b6" : "#db2777", + null: dark ? "#9ca3af" : "#6b7280", + key: dark ? "#e5e7eb" : "#1f2937", + bracket: dark ? "#9ca3af" : "#6b7280", + preview: dark ? "#9ca3af" : "#6b7280", + }; +} + +const monoStyle: React.CSSProperties = { + fontFamily: "monospace", +}; + +/** + * Renders a single primitive value with type-appropriate coloring. + */ +function PrimitiveValue({ value }: { value: unknown }) { + const colors = typeColors(); + + if (value === null || value === undefined) { + return ( + + {String(value)} + + ); + } + + switch (typeof value) { + case "string": + return ( + + "{value}" + + ); + case "number": + case "bigint": + return ( + + {String(value)} + + ); + case "boolean": + return ( + + {String(value)} + + ); + default: + return ( + + {String(value)} + + ); + } +} + +/** + * Returns a collapsed preview string for an object or array. + * e.g. `{name: "foo", count: 3}` or `[1, 2, 3]` + */ +function collapsedPreview(value: object): string { + if (Array.isArray(value)) { + if (value.length <= 5) { + const items = value.map((v) => primitivePreview(v)); + const preview = `[${items.join(", ")}]`; + if (preview.length <= 80) { + return preview; + } + } + return value.length === 0 ? "[]" : `Array(${value.length})`; + } + + const entries = Object.entries(value); + if (entries.length <= 3) { + const items = entries.map(([k, v]) => `${k}: ${primitivePreview(v)}`); + const preview = `{${items.join(", ")}}`; + if (preview.length <= 80) { + return preview; + } + } + return entries.length === 0 ? "{}" : `{…}`; +} + +function primitivePreview(value: unknown): string { + if (value === null) { + return "null"; + } + if (value === undefined) { + return "undefined"; + } + if (typeof value === "string") { + if (value.length > 30) { + return `"${value.slice(0, 30)}…"`; + } + return `"${value}"`; + } + if (isObject(value)) { + if (Array.isArray(value)) { + return `Array(${value.length})`; + } + return "{…}"; + } + return String(value); +} + +/** + * Renders an expandable tree node for an object or array value. + * Each nesting level is independently expandable/collapsible. + */ +function ObjectTree({ value, label }: { value: object; label?: string }) { + const [expanded, setExpanded] = useState(false); + const colors = typeColors(); + const isArray = Array.isArray(value); + const entries = isArray + ? value.map((v, i) => [String(i), v] as const) + : Object.entries(value); + + if (entries.length === 0) { + return ( + + {label !== undefined && ( + <> + + {label} + {": "} + + )} + + {isArray ? "[]" : "{}"} + + + ); + } + + return ( + + { + e.stopPropagation(); + setExpanded(!expanded); + }} + > + + {expanded ? "▼" : "▶"} + + {label !== undefined && ( + <> + {label} + {": "} + + )} + {!expanded && ( + + {collapsedPreview(value)} + + )} + + {expanded && ( + <> + + {isArray ? "[" : "{"} + + + {entries.map(([key, val], idx) => ( + + + + ))} + + + {isArray ? "]" : "}"} + + + )} + + ); +} + +/** + * Renders a single key-value entry. If the value is an object/array, it becomes + * an expandable ObjectTree. Otherwise it renders as a labeled primitive. + */ +function ValueNode({ label, value }: { label: string; value: unknown }) { + const colors = typeColors(); + + if (isObject(value)) { + return ; + } + + return ( + + + {label} + {": "} + + + ); +} + +/** + * Renders a single message part from console.log output. + * Strings render inline; objects/arrays render as expandable trees. + */ +function MessagePart({ value }: { value: unknown }) { + if (isObject(value)) { + return ; + } + + // Strings render without quotes at the top level (matching console.log behavior) + if (typeof value === "string") { + return {value}; + } + + return ; +} + +/** + * Top-level component that renders an array of console.log arguments. + * Each argument is rendered as a MessagePart, separated by spaces. + */ +export function ExpandableLogMessage({ messages }: { messages: unknown[] }) { + return ( + + {messages.map((msg, i) => ( + + {i > 0 && " "} + + + ))} + + ); +} + +/** + * Returns a plain-text summary of the messages for use in title/tooltip attributes. + */ +export function messageSummary(messages: unknown[]): string { + return messages + .map((msg) => { + if (msg === null) { + return "null"; + } + if (msg === undefined) { + return "undefined"; + } + if (isObject(msg)) { + try { + const json = JSON.stringify(msg); + return json.length > 500 ? json.slice(0, 500) + "…" : json; + } catch { + return String(msg); + } + } + return String(msg); + }) + .join(" "); +} diff --git a/packages/wrangler/e2e/unenv-preset/preset.test.ts b/packages/wrangler/e2e/unenv-preset/preset.test.ts index 1b87c48305b6..cd6c8f4bdeff 100644 --- a/packages/wrangler/e2e/unenv-preset/preset.test.ts +++ b/packages/wrangler/e2e/unenv-preset/preset.test.ts @@ -727,6 +727,27 @@ const localTestConfigs: TestConfig[] = [ }, }, ], + // node:readline (experimental, no default enable date) + [ + // TODO: add test for disabled by date (no date defined yet) + // TODO: add test for enabled by date (no date defined yet) + { + name: "readline enabled by flag", + compatibilityDate: "2024-09-23", + compatibilityFlags: ["enable_nodejs_readline_module", "experimental"], + expectRuntimeFlags: { + enable_nodejs_readline_module: true, + }, + }, + { + name: "readline disabled by flag", + compatibilityDate: "2024-09-23", + compatibilityFlags: ["disable_nodejs_readline_module", "experimental"], + expectRuntimeFlags: { + enable_nodejs_readline_module: false, + }, + }, + ], ].flat() as TestConfig[]; describe.each(localTestConfigs)( diff --git a/packages/wrangler/e2e/unenv-preset/worker/index.ts b/packages/wrangler/e2e/unenv-preset/worker/index.ts index 1eef830a4388..4dc283ad082e 100644 --- a/packages/wrangler/e2e/unenv-preset/worker/index.ts +++ b/packages/wrangler/e2e/unenv-preset/worker/index.ts @@ -1033,6 +1033,35 @@ export const WorkerdTests: Record void> = { ); } }, + + async testReadline() { + const readline = await import("node:readline"); + + for (const target of [readline, readline.default]) { + assertTypeOfProperties(target, { + Interface: "function", + clearLine: "function", + clearScreenDown: "function", + createInterface: "function", + cursorTo: "function", + emitKeypressEvents: "function", + moveCursor: "function", + promises: "object", + }); + } + }, + + async testReadlinePromises() { + const readlinePromises = await import("node:readline/promises"); + + for (const target of [readlinePromises, readlinePromises.default]) { + assertTypeOfProperties(target, { + Interface: "function", + Readline: "function", + createInterface: "function", + }); + } + }, }; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3be88f7621d..f92f66924fb6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2540,8 +2540,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: ^1.20260214.0 - version: 1.20260217.0 + specifier: ^1.20260218.0 + version: 1.20260218.0 devDependencies: '@types/node-unenv': specifier: npm:@types/node@^22.14.0 @@ -5221,12 +5221,6 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260217.0': - resolution: {integrity: sha512-t1KRT0j4gwLntixMoNujv/UaS89Q7+MPRhkklaSup5tNhl3zBZOIlasBUSir69eXetqLZu8sypx3i7zE395XXA==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260218.0': resolution: {integrity: sha512-3qEtKyUzAkr9a4VRV1DSgXkC1aTei+K3sHatSlfeSB2hf45R4fATlAjgCjoYcCThizWbH147OK5ou7qTsjkuEQ==} engines: {node: '>=16'} @@ -5239,12 +5233,6 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260217.0': - resolution: {integrity: sha512-9pEZ15BmELt0Opy79LTxUvbo55QAI4GnsnsvmgBxaQlc4P0dC8iycBGxbOpegkXnRx/LFj51l2zunfTo0EdATg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260218.0': resolution: {integrity: sha512-RqanxECcJtyLhdacQNhwg+4nRItBNj2SqVkdIVKHfTBO11HUCokEzzwC4MfbQhLqippwjJbhcS8RbVS0O2we/w==} engines: {node: '>=16'} @@ -5257,12 +5245,6 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260217.0': - resolution: {integrity: sha512-IrZfxQ4b/4/RDQCJsyoxKrCR+cEqKl81yZOirMOKoRrDOmTjn4evYXaHoLBh2PjUKY1Imly7ZiC6G1p0xNIOwg==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - '@cloudflare/workerd-linux-64@1.20260218.0': resolution: {integrity: sha512-qAFxkOELTtFYDk1LSPEF6J0F9RE4H4xGN+xZ3KeoT+MYNKGjUDtGAVoX17BhRxeXmI1jruQ4MnUy05wUHIWzRg==} engines: {node: '>=16'} @@ -5275,12 +5257,6 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260217.0': - resolution: {integrity: sha512-RGU1wq69ym4sFBVWhQeddZrRrG0hJM/SlZ5DwVDga/zBJ3WXxcDsFAgg1dToDfildTde5ySXN7jAasSmWko9rg==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260218.0': resolution: {integrity: sha512-2GeKyRbMlGXqaOrRuys0QtUMSpn20HUoCThU6aCfFvpBBPFS8+UzJVY4gmGZ2ugbL3WSYZW/MBfOnpBL9bvL7Q==} engines: {node: '>=16'} @@ -5293,12 +5269,6 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260217.0': - resolution: {integrity: sha512-4T65u1321z1Zet9n7liQsSW7g3EXM5SWIT7kJ/uqkEtkPnIzZBIowMQgkvL5W9SpGZks9t3mTQj7hiUia8Gq9Q==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - '@cloudflare/workerd-windows-64@1.20260218.0': resolution: {integrity: sha512-VFmKg7OxOloQJFfds11YXxuXThYiJ+lo6AsdjNtFczkW1FjYW1mennGpVPlv2jo66MmOhgKrfDhddq3wheXoIQ==} engines: {node: '>=16'} @@ -14819,11 +14789,6 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260217.0: - resolution: {integrity: sha512-6jVisS6wB6KbF+F9DVoDUy9p7MON8qZCFSaL8OcDUioMwknsUPFojUISu3/c30ZOZ24D4h7oqaahFc5C6huilw==} - engines: {node: '>=16'} - hasBin: true - workerd@1.20260218.0: resolution: {integrity: sha512-giHup2G8WN7h06rssk0XaXwmqYbcbAzs2nejUM3uWWvxLwK/KVt+aOi+MBuUIZ9cZgTmiB/wtO0rdlAUy4jLQQ==} engines: {node: '>=16'} @@ -16347,45 +16312,30 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20251210.0': optional: true - '@cloudflare/workerd-darwin-64@1.20260217.0': - optional: true - '@cloudflare/workerd-darwin-64@1.20260218.0': optional: true '@cloudflare/workerd-darwin-arm64@1.20251210.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260217.0': - optional: true - '@cloudflare/workerd-darwin-arm64@1.20260218.0': optional: true '@cloudflare/workerd-linux-64@1.20251210.0': optional: true - '@cloudflare/workerd-linux-64@1.20260217.0': - optional: true - '@cloudflare/workerd-linux-64@1.20260218.0': optional: true '@cloudflare/workerd-linux-arm64@1.20251210.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20260217.0': - optional: true - '@cloudflare/workerd-linux-arm64@1.20260218.0': optional: true '@cloudflare/workerd-windows-64@1.20251210.0': optional: true - '@cloudflare/workerd-windows-64@1.20260217.0': - optional: true - '@cloudflare/workerd-windows-64@1.20260218.0': optional: true @@ -26764,14 +26714,6 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20251210.0 '@cloudflare/workerd-windows-64': 1.20251210.0 - workerd@1.20260217.0: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260217.0 - '@cloudflare/workerd-darwin-arm64': 1.20260217.0 - '@cloudflare/workerd-linux-64': 1.20260217.0 - '@cloudflare/workerd-linux-arm64': 1.20260217.0 - '@cloudflare/workerd-windows-64': 1.20260217.0 - workerd@1.20260218.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20260218.0 diff --git a/tools/deployments/validate-changesets.ts b/tools/deployments/validate-changesets.ts index 943d211f2f7b..d03d7101cffa 100644 --- a/tools/deployments/validate-changesets.ts +++ b/tools/deployments/validate-changesets.ts @@ -35,6 +35,7 @@ export function validateChangesets( "@cloudflare/workers-shared", "@cloudflare/quick-edit", "@cloudflare/devprod-status-bot", + "@cloudflare/workers-playground", ]; if ( packages.get(release.name)?.["workers-sdk"]?.deploy &&