Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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/silent-words-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/vite-plugin-nitro-2": patch
---

Fix Cloudflare preset not generating functioning applications
43 changes: 43 additions & 0 deletions packages/start-nitro-v2-vite-plugin/src/cloudflare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const CLOUDFLARE_PRESETS = ["cloudflare-module", "cloudflare-pages", "cloudflare-durable"];

export function isCloudflarePreset(preset: string | undefined): boolean {
if (!preset) return false;
return CLOUDFLARE_PRESETS.some((cf) => preset.toLowerCase().includes(cf.toLowerCase()));
}

/**
* Generates the virtual entry content for Cloudflare Workers.
*
* Nitro's nested H3 apps strip the URL down to just the path
* (e.g., "/about" instead of "http://example.com/about").
* This causes h3's `fromWebHandler` to fail with "Invalid URL string".
*
* This handler reconstructs the full URL from headers before passing
* the request to the SolidStart handler.
*/
export function getCloudflareVirtualEntryContent(ssrEntry: string): string {
return `
import { defineEventHandler } from 'h3';
import handler from '${ssrEntry}';

export default defineEventHandler((event) => {
const headers = event.req.headers;
const getHeader = (name) => {
if (headers && typeof headers.get === 'function') return headers.get(name);
if (headers && typeof headers === 'object') return headers[name] || headers[name.toLowerCase()];
return null;
};
const host = getHeader('host') || 'localhost';
const proto = getHeader('x-forwarded-proto') || 'http';
const path = event.path || event.req.url || '/';
const fullUrl = proto + '://' + host + path;
const request = new Request(fullUrl, {
method: event.req.method,
headers: headers,
body: event.req.method !== 'GET' && event.req.method !== 'HEAD' ? event.req.body : undefined,
duplex: 'half'
});
return handler.fetch(request);
});
`;
}
29 changes: 23 additions & 6 deletions packages/start-nitro-v2-vite-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import {
copyPublicAssets,
createNitro,
type Nitro,
type NitroConfig,
NitroConfig,
prepare,
prerender,
} from "nitropack";
import { promises as fsp } from "node:fs";
import path, { dirname, resolve } from "node:path";
import type { PluginOption, Rollup } from "vite";
import { isCloudflarePreset, getCloudflareVirtualEntryContent } from "./cloudflare.js";

let ssrBundle: Rollup.OutputBundle;
let ssrEntryFile: string;
Expand Down Expand Up @@ -74,10 +75,13 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
await builder.build(server);

const virtualEntry = "#solid-start/entry";

const preset = nitroConfig?.preset ?? "node-server";

const resolvedNitroConfig: NitroConfig = {
compatibilityDate: "2024-11-19",
logLevel: 3,
preset: "node-server",
preset,
typescript: {
generateTsConfig: false,
generateRuntimeConfigTypes: false,
Expand All @@ -98,7 +102,9 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
baseURL: "/",
},
],
noExternals: false,
// For Cloudflare presets, bundle everything inline to avoid conditional export
// resolution issues (e.g., srvx's "workerd" export not being copied to output)
noExternals: isCloudflarePreset(preset),
renderer: virtualEntry,
rollupConfig: {
...nitroConfig?.rollupConfig,
Expand All @@ -110,9 +116,7 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
},
virtual: {
...nitroConfig?.virtual,
[virtualEntry]: `import { fromWebHandler } from 'h3'
import handler from '${ssrEntryFile}'
export default fromWebHandler(handler.fetch)`,
[virtualEntry]: getVirtualEntryContent(preset, ssrEntryFile),
},
};

Expand Down Expand Up @@ -144,6 +148,19 @@ export function nitroV2Plugin(nitroConfig?: UserNitroConfig): PluginOption {
];
}

function getVirtualEntryContent(preset: string, ssrEntry: string): string {
if (isCloudflarePreset(preset)) {
return getCloudflareVirtualEntryContent(ssrEntry);
}

return `
import { fromWebHandler } from 'h3';
import handler from '${ssrEntry}';

export default fromWebHandler(handler.fetch);
`;
}

export async function buildNitroEnvironment(nitro: Nitro, build: () => Promise<any>) {
await prepare(nitro);
await copyPublicAssets(nitro);
Expand Down
Loading