Skip to content
Closed
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
77 changes: 54 additions & 23 deletions packages/vite-plugin-cloudflare/src/asset-workers/asset-worker.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
// @ts-ignore
import AssetWorker from "@cloudflare/workers-shared/dist/asset-worker.mjs";
import { UNKNOWN_HOST } from "../shared";

interface Env {
__VITE_ASSET_EXISTS__: Fetcher;
__VITE_FETCH_ASSET__: Fetcher;
}

export default class CustomAssetWorker extends AssetWorker {
async fetch(request: Request): Promise<Response> {
Expand All @@ -17,29 +11,55 @@ export default class CustomAssetWorker extends AssetWorker {
return modifiedResponse;
}
async unstable_getByETag(
eTag: string
eTag: string,
request: Request
): Promise<{ readableStream: ReadableStream; contentType: string }> {
const url = new URL(eTag, UNKNOWN_HOST);
const response = await (
this as typeof AssetWorker as { env: Env }
).env.__VITE_FETCH_ASSET__.fetch(url);
const url = new URL(request.url);
url.pathname = eTag;
const pathRequest = new Request(url, request);
console.log(`AssetWorker: getByEtag(${pathRequest.url})`);
const response = await fetchAsset(pathRequest);

if (!response.body) {
throw new Error(`Unexpected error. No HTML found for ${eTag}.`);
const readableStream = response.body;
if (!readableStream) {
throw new Error(`Unexpected error. No content found for ${eTag}.`);
}

return { readableStream: response.body, contentType: "text/html" };
}
async unstable_exists(pathname: string): Promise<string | null> {
// We need this regex to avoid getting `//` as a pathname, which results in an invalid URL. Should this be fixed upstream?
const url = new URL(pathname.replace(/^\/{2,}/, "/"), UNKNOWN_HOST);
const response = await (
this as typeof AssetWorker as { env: Env }
).env.__VITE_ASSET_EXISTS__.fetch(url);
const exists = await response.json();
const contentType = response.headers.get("Content-Type");
if (!contentType) {
throw new Error(
`Unexpected error. Content type is missing from the for ${eTag}`
);
}

return exists ? pathname : null;
return { readableStream, contentType };
}
async unstable_exists(
pathname: string,
request: Request
): Promise<string | null> {
try {
const url = new URL(request.url);
url.pathname = pathname;
const pathRequest = new Request(url, request);
console.log(`AssetWorker: exists(${pathRequest.url}) PRE`);
const response = await fetchAsset(pathRequest);
const exists = response.status === 200;
console.log(
`AssetWorker: exists(${pathRequest.url}) POST`,
response.statusText,
response.status,
pathname,
response.headers.get("content-type")
);
return exists ? pathname : null;
} catch (e) {
console.log("AssetWorker: error");
console.log(e);
return null;
}
}

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'
Expand All @@ -50,3 +70,14 @@ export default class CustomAssetWorker extends AssetWorker {
return await super.unstable_canFetch(request);
}
}

function fetchAsset(request: Request) {
try {
const headers = new Headers(request.headers);
headers.set("__CF_REQUEST_TYPE_", "ASSET");
const newRequest = new Request(request, { headers });
return fetch(newRequest);
} catch (error) {
throw new Error(`Unexpected error. Failed to fetch asset: ${request.url}`);
}
}
138 changes: 116 additions & 22 deletions packages/vite-plugin-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import {
getFirstAvailablePort,
getOutputDirectory,
getRouterWorker,
isAssetFetch,
isWorkerFetch,
log,
toMiniflareRequest,
} from "./utils";
import { handleWebSocket } from "./websockets";
Expand Down Expand Up @@ -90,17 +93,17 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
// This only applies to this plugin so is safe to use while other plugins migrate to the Environment API
sharedDuringBuild: true,
config(userConfig, env) {
if (env.isPreview) {
// Short-circuit the whole configuration if we are in preview mode
return { appType: "custom" };
}

resolvedPluginConfig = resolvePluginConfig(
pluginConfig,
userConfig,
env
);

if (env.isPreview) {
// Short-circuit the whole configuration if we are in preview mode
return { appType: "custom" };
}

if (!workersConfigsWarningShown) {
workersConfigsWarningShown = true;
const workersConfigsWarning = getWarningForWorkersConfigs(
Expand Down Expand Up @@ -358,18 +361,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
}

await initRunners(resolvedPluginConfig, viteDevServer, miniflare);

const middleware = createMiddleware(
async ({ request }) => {
assert(miniflare, `Miniflare not defined`);
const routerWorker = await getRouterWorker(miniflare);

return routerWorker.fetch(toMiniflareRequest(request), {
redirect: "manual",
}) as any;
},
{ alwaysCallNext: false }
);
const entryWorker = await getRouterWorker(miniflare);

handleWebSocket(viteDevServer.httpServer, async () => {
assert(miniflare, `Miniflare not defined`);
Expand All @@ -378,11 +370,71 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {
return routerWorker.fetch;
});

return () => {
viteDevServer.middlewares.use((req, res, next) => {
middleware(req, res, next);
});
};
const preMiddleware = createMiddleware(
({ request, passThrough }) => {
const url = new URL(request.url);
if (url.pathname.startsWith("/@")) {
// URLs that start with `/@...` are special vite requests.
// We shouldn't pass them through the AssetWorker because it will response
// with a redirect to an encoded version `/%40...`.
log(
"pre-middleware",
request,
"special Vite request - pass-through"
);
passThrough();
} else if (
// workerType === "workers-only" ||
isAssetFetch(request) ||
isWorkerFetch(request)
) {
log("pre-middleware", request, "pass-through");
passThrough();
} else {
log("pre-middleware", request, "ask Entry Worker for response");
return entryWorker.fetch(toMiniflareRequest(request), {
redirect: "manual",
}) as any;
}
},
{ alwaysCallNext: false }
);

const postMiddleware = createMiddleware(
async ({ request, passThrough }) => {
if (
// userWorker &&
// (workerType === "workers-only" ||
// )
isWorkerFetch(request)
) {
// No assets (just a userWorker) or a specific user worker request
log("post-middleware", request, "ask User Worker for response");
return entryWorker.fetch(toMiniflareRequest(request)) as any;
} else if (isAssetFetch(request)) {
try {
log("post-middleware", request, "ask Vite to transform HTML");
const response = await tryHtmlTransform(viteDevServer, request);
if (response) {
return response;
}
} catch {
log(
"post-middleware",
request,
"unable to transform HTML - pass-through"
);
}
passThrough();
} else {
log("post-middleware", request, "pass-through");
passThrough;
}
}
);

viteDevServer.middlewares.use(preMiddleware);
return () => viteDevServer.middlewares.use(postMiddleware);
},
async configurePreviewServer(vitePreviewServer) {
const workerConfigs = getWorkerConfigs(vitePreviewServer.config.root);
Expand All @@ -394,6 +446,7 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin[] {

const miniflare = new Miniflare(
getPreviewMiniflareOptions(
resolvedPluginConfig,
vitePreviewServer,
workerConfigs,
pluginConfig.persistState ?? true,
Expand Down Expand Up @@ -938,3 +991,44 @@ function hasDotDevDotVarsFileChanged(
return false;
});
}

async function tryHtmlTransform(
viteDevServer: vite.ViteDevServer,
request: Request
) {
const fileRoot = viteDevServer.config.root;
const { pathname } = new URL(request.url);
const filePath = path.join(fileRoot, pathname);

if (!filePath.endsWith(".html") || !isFile(filePath)) {
return null;
}

try {
console.log("Processing HTML file");
const content = await fsp.readFile(filePath, "utf-8");
const transformed = await viteDevServer.transformIndexHtml(
pathname,
content
);

return new Response(transformed, {
headers: { "Content-Type": "text/html" },
});
} catch (error) {
throw new Error(
`Unexpected error. Failed to load and transform ${pathname} - ${error}`
);
}
}

async function isFile(maybeFilePath: string) {
try {
const exists = (await fsp.stat(maybeFilePath)).isFile();
if (!exists) {
return null;
}
} catch (error) {
return null;
}
}
Loading
Loading