Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/drop-live-reload-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"miniflare": major
---

Drop `/cdn-cgi/mf/reload` live reload endpoint and `liveReload` option

The built-in live reload mechanism has been removed from Miniflare. This included a WebSocket endpoint at `/cdn-cgi/mf/reload`, the `liveReload` option, and the automatic injection of a live reload `<script>` tag into HTML responses. For context, Wrangler and the Vite plugin both implement their own independent live reload mechanisms.
17 changes: 17 additions & 0 deletions .changeset/remap-local-testing-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"miniflare": major
---

Remap local testing paths to avoid collision with production `/cdn-cgi` routes

All miniflare-internal endpoints have moved to more consistent paths. Paths that need to remain reachable over tunnels now live outside `/cdn-cgi/`:

- `/cdn-cgi/platform-proxy` → `/cdn-cgi/local/platform-proxy`
- `/cdn-cgi/handler/scheduled` → `/cdn-cgi/local/scheduled`
- `/cdn-cgi/handler/email` → `/cdn-cgi/local/email`
- `/cdn-cgi/explorer/*` → `/cdn-cgi/local/explorer/*`
- `/cdn-cgi/mf/scheduled` → removed (was already deprecated)
- `/cdn-cgi/mf/stream/*` → `/__cf_local/stream/*`
- `/cdn-cgi/mf/imagedelivery/*` → `/__cf_local/imagedelivery/*`

Wrangler and the Vite plugin will add transparent path rewrites so the old paths continue to work for users of those tools.
18 changes: 18 additions & 0 deletions .changeset/rewrite-legacy-local-testing-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"wrangler": patch
"@cloudflare/vite-plugin": patch
---

Rewrite local testing paths (`/cdn-cgi/*`)

Miniflare v5 moved its internal local testing endpoints to `/cdn-cgi/local/*` (and `/__cf_local/*` for endpoints that must remain reachable over tunnels) to prevent any potential collision with production routes. `wrangler dev` and the Vite plugin now transparently rewrite the old paths to the new ones, meaning you can continue to use the old paths without issue.

These are the new paths:

- `/cdn-cgi/handler/scheduled` → `/cdn-cgi/local/scheduled`
- `/cdn-cgi/handler/email` → `/cdn-cgi/local/email`
- `/cdn-cgi/explorer/*` → `/cdn-cgi/local/explorer/*`
- `/cdn-cgi/mf/scheduled` → `/cdn-cgi/local/scheduled` (Note `/cdn-cgi/mf/scheduled` is already deprecated)
- `/cdn-cgi/mf/stream/*` → `/__cf_local/stream/*`
- `/cdn-cgi/mf/imagedelivery/*` → `/__cf_local/imagedelivery/*`
- `/cdn-cgi/platform-proxy` → `/cdn-cgi/local/platform-proxy`
5 changes: 2 additions & 3 deletions fixtures/worker-app/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,8 @@ export default {
/**
* Handle a scheduled event.
*
* If developing using `--local` mode, you can trigger this scheduled event via a CURL.
* E.g. `curl "http://localhost:8787/cdn-cgi/mf/scheduled"`.
* See the Miniflare docs: https://miniflare.dev/core/scheduled.
* When developing locally, you can trigger this scheduled event via a CURL.
* E.g. `curl "http://localhost:8787/cdn-cgi/local/scheduled"`.
*/
scheduled(event, env, ctx) {
ctx.waitUntil(Promise.resolve(event.scheduledTime));
Expand Down
33 changes: 21 additions & 12 deletions packages/local-explorer-ui/src/__e2e__/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { readFile, unlink } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
LOCAL_EXPLORER_API_PATH,
LOCAL_EXPLORER_BASE_PATH,
} from "../constants";
import { page, viteUrl, workerUrl } from "./setup";

export { page, viteUrl };
Expand Down Expand Up @@ -57,7 +61,7 @@ export async function seedWorkflow(workflowName: string): Promise<{
const workflowId = `e2e-test-${Date.now()}`;

await fetch(
`${workerUrl}/cdn-cgi/explorer/api/workflows/${workflowName}/instances`,
`${workerUrl}${LOCAL_EXPLORER_API_PATH}/workflows/${workflowName}/instances`,
{
body: JSON.stringify({
id: workflowId,
Expand All @@ -79,9 +83,12 @@ export async function seedWorkflow(workflowName: string): Promise<{
* Delete all instances of a workflow via the explorer API.
*/
export async function cleanupWorkflow(workflowName: string): Promise<void> {
await fetch(`${workerUrl}/cdn-cgi/explorer/api/workflows/${workflowName}`, {
method: "DELETE",
});
await fetch(
`${workerUrl}${LOCAL_EXPLORER_API_PATH}/workflows/${workflowName}`,
{
method: "DELETE",
}
);
}

/**
Expand All @@ -96,15 +103,15 @@ const WAIT_OPTIONS = {
* Navigate to a KV namespace.
*/
export async function navigateToKV(namespaceId: string): Promise<void> {
await navigateTo(`/cdn-cgi/explorer/kv/${namespaceId}`);
await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/kv/${namespaceId}`);
await waitForPageLoad();
}

/**
* Navigate to an R2 bucket.
*/
export async function navigateToR2Bucket(bucketName: string): Promise<void> {
await navigateTo(`/cdn-cgi/explorer/r2/${bucketName}`);
await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/r2/${bucketName}`);
await waitForPageLoad();
}

Expand All @@ -116,7 +123,7 @@ export async function navigateToR2Object(
objectKey: string
): Promise<void> {
await navigateTo(
`/cdn-cgi/explorer/r2/${bucketName}/object/${encodeURIComponent(objectKey)}`
`${LOCAL_EXPLORER_BASE_PATH}/r2/${bucketName}/object/${encodeURIComponent(objectKey)}`
);
await waitForPageLoad();
}
Expand All @@ -128,7 +135,7 @@ export async function navigateToD1(
databaseId: string,
table?: string
): Promise<void> {
let path = `/cdn-cgi/explorer/d1/${databaseId}`;
let path = `${LOCAL_EXPLORER_BASE_PATH}/d1/${databaseId}`;
if (table) {
path += `?table=${encodeURIComponent(table)}`;
}
Expand All @@ -141,15 +148,15 @@ export async function navigateToD1(
* Navigate to a Durable Object class.
*/
export async function navigateToDOClass(className: string): Promise<void> {
await navigateTo(`/cdn-cgi/explorer/do/${className}`);
await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/do/${className}`);
await waitForPageLoad();
}

/**
* Navigate to a Workflow instances list page.
*/
export async function navigateToWorkflow(workflowName: string): Promise<void> {
await navigateTo(`/cdn-cgi/explorer/workflows/${workflowName}`);
await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/workflows/${workflowName}`);
await waitForPageLoad();
}

Expand All @@ -164,7 +171,7 @@ export async function navigateToDOObject(
objectId: string,
table?: string
): Promise<void> {
let path = `/cdn-cgi/explorer/do/${className}/${objectId}`;
let path = `${LOCAL_EXPLORER_BASE_PATH}/do/${className}/${objectId}`;
if (table) {
path += `?table=${encodeURIComponent(table)}`;
}
Expand Down Expand Up @@ -200,7 +207,9 @@ export async function navigateToDOObjectByName(

// Extract the object ID from the current URL after navigation.
const objectPath = new URL(page.url()).pathname;
const match = objectPath.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([^/?#]+)/);
const match = objectPath.match(
/\/cdn-cgi\/local\/explorer\/do\/[^/]+\/([^/?#]+)/
);
if (!match || !match[1]) {
throw new Error(`Could not extract object ID from URL path: ${objectPath}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import { describe, test, vi } from "vitest";
import { LOCAL_EXPLORER_API_PATH } from "../../constants";
import {
copyTextToClipboard,
createLocalExplorerPrompt,
getLocalExplorerApiEndpoint,
} from "../../utils/agent-prompt";

const TEST_ORIGIN = "http://localhost:8787";
const TEST_API_ENDPOINT = `${TEST_ORIGIN}${LOCAL_EXPLORER_API_PATH}`;

describe("llm-prompt utils", () => {
test("builds api endpoint from origin and api path", ({ expect }) => {
expect(
getLocalExplorerApiEndpoint(
"http://localhost:8787",
"/cdn-cgi/explorer/api"
)
).toBe("http://localhost:8787/cdn-cgi/explorer/api");
getLocalExplorerApiEndpoint(TEST_ORIGIN, LOCAL_EXPLORER_API_PATH)
).toBe(TEST_API_ENDPOINT);
});

test("generates prompt text with resolved api endpoint", ({ expect }) => {
const prompt = createLocalExplorerPrompt(
"http://localhost:8787/cdn-cgi/explorer/api"
);
const prompt = createLocalExplorerPrompt(TEST_API_ENDPOINT);

expect(prompt).toContain(`API endpoint: ${TEST_API_ENDPOINT}`);
expect(prompt).toContain(
"API endpoint: http://localhost:8787/cdn-cgi/explorer/api"
);
expect(prompt).toContain(
"Fetch the OpenAPI schema from http://localhost:8787/cdn-cgi/explorer/api"
`Fetch the OpenAPI schema from ${TEST_API_ENDPOINT}`
);
});

Expand Down
3 changes: 2 additions & 1 deletion packages/local-explorer-ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import DOIcon from "../assets/icons/durable-objects.svg?react";
import KVIcon from "../assets/icons/kv.svg?react";
import R2Icon from "../assets/icons/r2.svg?react";
import WorkflowsIcon from "../assets/icons/workflows.svg?react";
import { LOCAL_EXPLORER_BASE_PATH } from "../constants";
import { loadGroupState, saveGroupState } from "../utils/sidebar-state";
import { getNextThemeMode } from "../utils/theme-state";
import { SidebarGroupPopup } from "./SidebarGroupPopup";
Expand Down Expand Up @@ -198,7 +199,7 @@ export function AppSidebar({
<div className="flex w-full items-center justify-between">
<a
className="box-border flex items-center gap-2.5 px-1"
href="/cdn-cgi/explorer/"
href={`${LOCAL_EXPLORER_BASE_PATH}/`}
>
<CloudflareLogo
className={cn(
Expand Down
9 changes: 9 additions & 0 deletions packages/local-explorer-ui/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Canonical path definitions for the local explorer.
*
* These mirror the values in packages/miniflare/src/workers/core/constants.ts
* but cannot be imported directly due to circular dependency
* (miniflare depends on @cloudflare/local-explorer-ui).
*/
export const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/local/explorer";
export const LOCAL_EXPLORER_API_PATH = `${LOCAL_EXPLORER_BASE_PATH}/api`;
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { NotFound } from "../../../components/NotFound";
import { R2ObjectTable } from "../../../components/R2ObjectTable";
import { R2UploadDialog } from "../../../components/R2UploadDialog";
import { ResourceError } from "../../../components/ResourceError";
import { LOCAL_EXPLORER_API_PATH } from "../../../constants";
import { withMinimumDelay } from "../../../utils/async";
import type { R2Object } from "../../../api";

Expand Down Expand Up @@ -215,7 +216,7 @@ function BucketView(): JSX.Element {

function handleDownload(keys: string[]): void {
for (const key of keys) {
const downloadUrl = `/cdn-cgi/explorer/api/r2/buckets/${encodeURIComponent(params.bucketName)}/objects/${encodeURIComponent(key)}`;
const downloadUrl = `${LOCAL_EXPLORER_API_PATH}/r2/buckets/${encodeURIComponent(params.bucketName)}/objects/${encodeURIComponent(key)}`;
const link = document.createElement("a");
link.href = downloadUrl;
link.download = key.split("/").pop() || "download";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Breadcrumbs } from "../../../components/Breadcrumbs";
import { CopyButton } from "../../../components/CopyButton";
import { NotFound } from "../../../components/NotFound";
import { ResourceError } from "../../../components/ResourceError";
import { LOCAL_EXPLORER_API_PATH } from "../../../constants";
import { formatDate, formatSize } from "../../../utils/format";
import type { R2HeadObjectResult } from "../../../api";

Expand Down Expand Up @@ -148,7 +149,7 @@ function ObjectDetailView(): JSX.Element {
const [error, setError] = useState<string | null>(null);

function handleDownload(): void {
const downloadUrl = `/cdn-cgi/explorer/api/r2/buckets/${encodeURIComponent(params.bucketName)}/objects/${encodeURIComponent(loaderData.objectKey)}`;
const downloadUrl = `${LOCAL_EXPLORER_API_PATH}/r2/buckets/${encodeURIComponent(params.bucketName)}/objects/${encodeURIComponent(loaderData.objectKey)}`;
const link = document.createElement("a");
link.href = downloadUrl;
link.download = loaderData.objectKey.split("/").pop() || "download";
Expand Down
3 changes: 2 additions & 1 deletion packages/local-explorer-ui/tsconfig.e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"src/__e2e__",
"src/__e2e__/global-setup.ts",
"src/__e2e__/setup.ts",
"../../fixtures/shared/src/run-wrangler-long-lived.ts"
"../../fixtures/shared/src/run-wrangler-long-lived.ts",
"src/constants.ts"
]
}
10 changes: 4 additions & 6 deletions packages/local-explorer-ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import svgr from "vite-plugin-svgr";

// Canonical definitions in packages/miniflare/src/plugins/core/constants.ts
// Cannot import from miniflare directly due to circular dependency
// (miniflare depends on @cloudflare/local-explorer-ui)
const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/explorer";
const LOCAL_EXPLORER_API_PATH = `${LOCAL_EXPLORER_BASE_PATH}/api`;
import {
LOCAL_EXPLORER_API_PATH,
LOCAL_EXPLORER_BASE_PATH,
} from "./src/constants";

export default defineConfig({
plugins: [
Expand Down
2 changes: 1 addition & 1 deletion packages/miniflare/scripts/filter-openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const LOCAL_EXPLORER_INFO = {
const LOCAL_EXPLORER_SERVERS = [
{
description: "Local Explorer",
url: "/cdn-cgi/explorer/api",
url: "/cdn-cgi/local/explorer/api",
},
];

Expand Down
31 changes: 5 additions & 26 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ import {
CacheHeaders,
CoreBindings,
CoreHeaders,
CorePaths,
LogLevel,
Mutex,
SharedHeaders,
Expand Down Expand Up @@ -1011,7 +1010,6 @@ export class Miniflare {
readonly #disposeController: AbortController;
#loopbackServer?: StoppableServer;
#loopbackHost?: string;
readonly #liveReloadServer: WebSocketServer;
readonly #webSocketServer: WebSocketServer;
readonly #webSocketExtraHeaders: WeakMap<http.IncomingMessage, Headers>;
readonly #devRegistry: DevRegistry;
Expand Down Expand Up @@ -1085,7 +1083,6 @@ export class Miniflare {
);
}

this.#liveReloadServer = new WebSocketServer({ noServer: true });
this.#webSocketServer = new WebSocketServer({
noServer: true,
// Disable automatic handling of `Sec-WebSocket-Protocol` header,
Expand Down Expand Up @@ -1170,10 +1167,6 @@ export class Miniflare {
}

#handleReload() {
// Reload all connected live reload clients
for (const ws of this.#liveReloadServer.clients) {
ws.close(1012, "Service Restart");
}
// Close all existing web sockets on reload
for (const ws of this.#webSocketServer.clients) {
ws.close(1012, "Service Restart");
Expand Down Expand Up @@ -1722,18 +1715,7 @@ export class Miniflare {
socket: Duplex,
head: Buffer
) => {
// Only interested in pathname so base URL doesn't matter
const { pathname } = new URL(req.url ?? "", "http://localhost");

// If this is the path for live-reload, handle the request
if (pathname === CorePaths.LIVE_RELOAD) {
this.#liveReloadServer.handleUpgrade(req, socket, head, (ws) => {
this.#liveReloadServer.emit("connection", ws, req);
});
return;
}

// Otherwise, try handle the request in a worker
// Try handle the request in a worker
const response = await this.#handleLoopback(req);

// Check web socket response was returned
Expand Down Expand Up @@ -1834,8 +1816,7 @@ export class Miniflare {
// This function must be run with `#runtimeMutex` held

// Start loopback server (how the runtime accesses Node.js) using the same
// host as the main runtime server. This means we can use the loopback
// server for live reload updates too.
// host as the main runtime server.
const configuredHost = this.#sharedOpts.core.host ?? DEFAULT_HOST;
const loopbackHost = resolveLocalhost(configuredHost) ?? configuredHost;
// If we've already started the loopback server...
Expand Down Expand Up @@ -2352,7 +2333,6 @@ export class Miniflare {
)
? `${RPC_PROXY_SERVICE_NAME}:${this.#workerOpts[0].core.name}`
: getUserServiceName(this.#workerOpts[0].core.name),
loopbackPort,
tmpPath: this.#tmpPath,
log: this.#log,
proxyBindings,
Expand Down Expand Up @@ -3209,11 +3189,10 @@ export class Miniflare {
} catch {}

await this.#stopLoopbackServer();
// Close WebSocket servers so any connected clients are disconnected
// and their sockets don't keep the event loop alive. These use
// `noServer: true` so they don't own an HTTP server, but connected
// Close the WebSocket server so any connected clients are disconnected
// and their sockets don't keep the event loop alive. It uses
// `noServer: true` so it doesn't own an HTTP server, but connected
// WebSocket clients still hold open sockets.
this.#liveReloadServer.close();
this.#webSocketServer.close();
// Best-effort cleanup: on Windows, workerd may not release file handles
// immediately after disposal, causing EBUSY errors. The temp directory
Expand Down
Loading
Loading