Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fuzzy-geese-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/vite-plugin": patch
---

Switch all instances of `miniflare.getWorker()` followed by `worker.fetch()` to use `miniflare.dispatchFetch()`. This means that the Vite plugin now emulates Cloudflare's response encoding in the same way as Wrangler.
2 changes: 2 additions & 0 deletions packages/miniflare/src/workers/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const CoreHeaders = {
ERROR_STACK: "MF-Experimental-Error-Stack",
ROUTE_OVERRIDE: "MF-Route-Override",
CF_BLOB: "MF-CF-Blob",
/** Used by the Vite plugin to pass through the original `sec-fetch-mode` header */
SEC_FETCH_MODE: "MF-Sec-Fetch-Mode",

// API Proxy
OP_SECRET: "MF-Op-Secret",
Expand Down
6 changes: 2 additions & 4 deletions packages/miniflare/src/workers/core/entry.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,11 @@ function getUserRequest(
// https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#accept-encoding
request.headers.set("Accept-Encoding", "br, gzip");

// `miniflare.dispatchFetch(request)` strips any `sec-fetch-mode` header. This allows clients to
// send it over a `x-mf-sec-fetch-mode` header instead (currently required by `vite preview`)
const secFetchMode = request.headers.get("X-Mf-Sec-Fetch-Mode");
const secFetchMode = request.headers.get(CoreHeaders.SEC_FETCH_MODE);
if (secFetchMode) {
request.headers.set("Sec-Fetch-Mode", secFetchMode);
}
request.headers.delete("X-Mf-Sec-Fetch-Mode");
request.headers.delete(CoreHeaders.SEC_FETCH_MODE);

if (rewriteHeadersFromOriginalUrl) {
request.headers.set("Host", url.host);
Expand Down
20 changes: 6 additions & 14 deletions packages/vite-plugin-cloudflare/src/cloudflare-environment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from "node:assert";
import * as util from "node:util";
import { CoreHeaders } from "miniflare";
import * as vite from "vite";
import { additionalModuleRE } from "./plugins/additional-modules";
import { VIRTUAL_WORKER_ENTRY } from "./plugins/virtual-modules";
Expand All @@ -11,13 +12,7 @@ import {
} from "./shared";
import { getOutputDirectory } from "./utils";
import type { WorkerConfig, WorkersResolvedConfig } from "./plugin-config";
import type { Fetcher } from "@cloudflare/workers-types/experimental";
import type {
MessageEvent,
Miniflare,
ReplaceWorkersTypes,
WebSocket,
} from "miniflare";
import type { MessageEvent, Miniflare, WebSocket } from "miniflare";
import type { FetchFunctionOptions } from "vite/module-runner";

export const MAIN_ENTRY_NAME = "index";
Expand Down Expand Up @@ -85,7 +80,6 @@ function createHotChannel(

export class CloudflareDevEnvironment extends vite.DevEnvironment {
#webSocketContainer: { webSocket?: WebSocket };
#worker?: ReplaceWorkersTypes<Fetcher>;

constructor(name: string, config: vite.ResolvedConfig) {
// It would be good if we could avoid passing this object around and mutating it
Expand All @@ -98,16 +92,15 @@ export class CloudflareDevEnvironment extends vite.DevEnvironment {
}

async initRunner(
worker: ReplaceWorkersTypes<Fetcher>,
miniflare: Miniflare,
workerConfig: WorkerConfig,
isEntryWorker: boolean
) {
this.#worker = worker;

const response = await this.#worker.fetch(
const response = await miniflare.dispatchFetch(
new URL(INIT_PATH, UNKNOWN_HOST),
{
headers: {
[CoreHeaders.ROUTE_OVERRIDE]: workerConfig.name,
[WORKER_ENTRY_PATH_HEADER]: encodeURIComponent(workerConfig.main),
[IS_ENTRY_WORKER_HEADER]: String(isEntryWorker),
upgrade: "websocket",
Expand Down Expand Up @@ -271,15 +264,14 @@ export function initRunners(
Object.entries(resolvedPluginConfig.workers).map(
async ([environmentName, workerConfig]) => {
debuglog("Initializing worker:", workerConfig.name);
const worker = await miniflare.getWorker(workerConfig.name);
const isEntryWorker =
environmentName === resolvedPluginConfig.entryWorkerEnvironmentName;

return (
viteDevServer.environments[
environmentName
] as CloudflareDevEnvironment
).initRunner(worker, workerConfig, isEntryWorker);
).initRunner(miniflare, workerConfig, isEntryWorker);
}
)
);
Expand Down
39 changes: 18 additions & 21 deletions packages/vite-plugin-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,11 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {

// The HTTP server is not available in middleware mode
if (viteDevServer.httpServer) {
handleWebSocket(viteDevServer.httpServer, async () => {
assert(miniflare, `Miniflare not defined`);
const entryWorker = await miniflare.getWorker(entryWorkerName);

return entryWorker.fetch;
});
handleWebSocket(
viteDevServer.httpServer,
miniflare,
entryWorkerName
);
}

const staticRouting: StaticRouting | undefined =
Expand All @@ -283,9 +282,9 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
);
const userWorkerHandler = createRequestHandler(async (request) => {
assert(miniflare, `Miniflare not defined`);
const userWorker = await miniflare.getWorker(entryWorkerName);
request.headers.set(CoreHeaders.ROUTE_OVERRIDE, entryWorkerName);

return userWorker.fetch(request, { redirect: "manual" });
return miniflare.dispatchFetch(request, { redirect: "manual" });
});

preMiddleware = async (req, res, next) => {
Expand Down Expand Up @@ -378,17 +377,19 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
assert(miniflare, `Miniflare not defined`);

if (req[kRequestType] === "asset") {
const assetWorker =
await miniflare.getWorker(ASSET_WORKER_NAME);
request.headers.set(
CoreHeaders.ROUTE_OVERRIDE,
ASSET_WORKER_NAME
);

return assetWorker.fetch(request, { redirect: "manual" });
return miniflare.dispatchFetch(request, { redirect: "manual" });
} else {
const routerWorker =
await miniflare.getWorker(ROUTER_WORKER_NAME);
request.headers.set(
CoreHeaders.ROUTE_OVERRIDE,
ROUTER_WORKER_NAME
);

return routerWorker.fetch(request, {
redirect: "manual",
});
return miniflare.dispatchFetch(request, { redirect: "manual" });
}
})
);
Expand Down Expand Up @@ -441,11 +442,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
});
}

handleWebSocket(vitePreviewServer.httpServer, () => {
assert(miniflare, `Miniflare not defined`);

return miniflare.dispatchFetch;
});
handleWebSocket(vitePreviewServer.httpServer, miniflare);

// In preview mode we put our middleware at the front of the chain so that all assets are handled in Miniflare
vitePreviewServer.middlewares.use(
Expand Down
3 changes: 2 additions & 1 deletion packages/vite-plugin-cloudflare/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from "node:path";
import { createRequest, sendResponse } from "@remix-run/node-fetch-server";
import {
CoreHeaders,
Request as MiniflareRequest,
Response as MiniflareResponse,
} from "miniflare";
Expand Down Expand Up @@ -91,7 +92,7 @@ function toMiniflareRequest(request: Request): MiniflareRequest {
// Undici sets the `Sec-Fetch-Mode` header to `cors` so we capture it in a custom header to be converted back later.
const secFetchMode = request.headers.get("Sec-Fetch-Mode");
if (secFetchMode) {
request.headers.set("X-Mf-Sec-Fetch-Mode", secFetchMode);
request.headers.set(CoreHeaders.SEC_FETCH_MODE, secFetchMode);
}
return new MiniflareRequest(request.url, {
method: request.method,
Expand Down
19 changes: 11 additions & 8 deletions packages/vite-plugin-cloudflare/src/websockets.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { createHeaders } from "@remix-run/node-fetch-server";
import { coupleWebSocket } from "miniflare";
import { CoreHeaders, coupleWebSocket } from "miniflare";
import { WebSocketServer } from "ws";
import { UNKNOWN_HOST } from "./shared";
import type { MaybePromise } from "./utils";
import type { Fetcher } from "@cloudflare/workers-types/experimental";
import type { ReplaceWorkersTypes } from "miniflare";
import type { Miniflare } from "miniflare";
import type { IncomingMessage } from "node:http";
import type { Duplex } from "node:stream";
import type * as vite from "vite";

/**
* This function handles 'upgrade' requests to the Vite HTTP server and forwards WebSocket events between the client and Worker environments.
* Handles 'upgrade' requests to the Vite HTTP server and forwards WebSocket events between the client and Worker environments.
*/
export function handleWebSocket(
httpServer: vite.HttpServer,
getFetcher: () => MaybePromise<ReplaceWorkersTypes<Fetcher>["fetch"]>
miniflare: Miniflare,
entryWorkerName?: string
) {
const nodeWebSocket = new WebSocketServer({ noServer: true });

Expand All @@ -29,8 +28,12 @@ export function handleWebSocket(
}

const headers = createHeaders(request);
const fetcher = await getFetcher();
const response = await fetcher(url, {

if (entryWorkerName) {
headers.set(CoreHeaders.ROUTE_OVERRIDE, entryWorkerName);
}

const response = await miniflare.dispatchFetch(url, {
headers,
method: request.method,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,4 @@ export default class CustomAssetWorker extends AssetWorker<Env> {

return response.json() as Promise<string | null>;
}
override async unstable_canFetch(request: Request) {
// the 'sec-fetch-mode: navigate' header is stripped by something on its way into this worker
// so we restore it from 'x-mf-sec-fetch-mode'
const secFetchMode = request.headers.get("X-Mf-Sec-Fetch-Mode");

if (secFetchMode) {
request.headers.set("Sec-Fetch-Mode", secFetchMode);
}

return await super.unstable_canFetch(request);
}
Comment on lines -56 to -66
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer needed because it's handled in Miniflare.

}
Loading