Skip to content

Commit 6dbb7e8

Browse files
feat(start): expose a Fetchable SSR service entry
Let provider Vite plugins adopt Solid's normal SSR environment while preserving the standalone server artifact and custom handler API. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 06aadc6 commit 6dbb7e8

6 files changed

Lines changed: 135 additions & 42 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'vite-plugin-solid': patch
3+
---
4+
5+
Start mode's generated request handler now default-exports a Fetchable
6+
`{ fetch(request) }` object alongside its named `handleRequest` export.
7+
Deployment integrations that follow the web-standard Fetchable convention
8+
can consume the virtual handler or built server entry without a wrapper.
9+
The `fetch` method intentionally ignores provider arguments after the request
10+
instead of forwarding them as Solid handler options.
11+
12+
The normal `ssr` environment now exposes that handler as its `index` service
13+
entry. Provider Vite plugins can adopt the same environment for development
14+
and production without `start.external`, a custom source entry, or explicit
15+
Rollup input. Standalone builds continue to emit `dist/server/server.js`.

README.md

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,23 @@ With `ssr: true` — **SSR start mode**:
200200
client — e.g. @cloudflare/vite-plugin — with no hand-written ordering
201201
plugin; setups without another orchestrator keep Vite's stock
202202
build-everything behavior, just client-first.
203-
- **Prod**: the server bundle's entry is `virtual:solid-ssr-handler`, whose
204-
`handleRequest(request)` export maps a web-standard `Request` to a
205-
streamed `Response` — adapter-agnostic, so any node server / worker /
206-
runtime mounts SSR in one line:
203+
- **Prod**: the server bundle's entry is `virtual:solid-ssr-handler`.
204+
Its named `handleRequest(request)` export maps a web-standard `Request`
205+
to a streamed `Response`; its default `{ fetch(request) }` export provides
206+
the same handler in the Fetchable shape used by Workers, Nitro, Netlify
207+
Functions, Bun, and `deno serve`:
207208

208209
```js
209-
import { handleRequest } from './dist/server/server.js';
210+
import app, { handleRequest } from './dist/server/server.js';
210211
// serve dist/client statically, everything else:
211212
const response = await handleRequest(request);
213+
const sameResponse = await app.fetch(request);
212214
```
213215

216+
The Fetchable wrapper deliberately accepts only the request. Hosts may pass
217+
environment or execution-context arguments after it; those are not the
218+
Solid options accepted by `handleRequest`'s second parameter.
219+
214220
- **Preview**: `vite build && vite preview` runs the production artifact
215221
with no server file — Vite's preview statics serve `dist/client`, and
216222
everything else (pages, the server-function endpoint, middleware)
@@ -402,31 +408,23 @@ server-function middleware pre-loads the referenced module, then dispatches
402408
through the same handler), so one middleware chain and one request event
403409
front pages and server functions identically.
404410

405-
**`external: true`** hands the server side of start mode to a host integration
406-
that owns the server environment — build wiring and HTTP serving alike. The
407-
plugin skips its start-mode server-build config (no `dist/server` output or
408-
builder flag; the host's orchestrator drives the server build) and stands
409-
its dev middlewares down (SSR serving and the server-function endpoint
410-
both). Start mode still provides everything the host loads through its own
411-
environment: the generated entries, the client manifest, and the
412-
`virtual:solid-ssr-handler` request handler — which self-serves in dev,
413-
inlining the entry graph's CSS through a virtual dev-styles module (HMR
414-
included) and composing the server-function endpoint, so
415-
`handleRequest(request)` is the whole contract in dev exactly as in
416-
production.
417-
418-
Three switches cover host-owned setups, broadest first:
419-
420-
1. **Nothing — capability detection.** When a provider (e.g.
421-
@cloudflare/vite-plugin) replaces the dev server's `ssr` environment with
422-
its own non-runnable one, the plugin detects that and stands the dev
423-
middlewares down automatically; the handler self-serves as above. Zero
424-
config.
425-
2. **`start.external: true`** — the explicit whole-server switch: everything
426-
detection does, plus skipping the server-build wiring. Also needed when
427-
the provider owns a _differently named_ environment (not `ssr`), which
428-
detection can't see.
429-
3. **[`serverFunctions.devMiddleware: false`](#optionsserverfunctions)**
411+
The normal `ssr` environment exposes the default Fetchable handler as its
412+
`index` service entry in development and production. Provider Vite plugins
413+
can adopt that environment directly: they supply its runtime and build
414+
orchestration while Solid continues to supply the application entry,
415+
manifest, middleware, and server-function dispatch. When a provider replaces
416+
the development environment with a non-runnable one, Solid detects that
417+
ownership and stands its HTTP middlewares down automatically.
418+
419+
Two explicit switches remain for custom host setups:
420+
421+
1. **`start.external: true`** — hands the whole server side to a host that
422+
does not adopt Solid's normal `ssr` environment. Solid skips its
423+
server-build wiring and stands its development middlewares down, while
424+
continuing to provide the generated entries, client manifest, and
425+
`virtual:solid-ssr-handler`. This is mainly for differently named or
426+
independently configured environments.
427+
2. **[`serverFunctions.devMiddleware: false`](#optionsserverfunctions)**
430428
the narrow, endpoint-only switch: keeps start mode's server build and SSR
431429
serving, hands only server-function dispatch in dev to the host. For
432430
setups without `start`, or when only the endpoint should move.

examples/turnkey/test/run.mjs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
// (default: all)
110110

111111
import { spawn, execSync } from 'node:child_process';
112-
import { fileURLToPath } from 'node:url';
112+
import { fileURLToPath, pathToFileURL } from 'node:url';
113113
import path from 'node:path';
114114
import { rmSync, existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
115115
import http from 'node:http';
@@ -926,6 +926,31 @@ async function runProdMode() {
926926
// The virtual handler's manifest import must keep the registrations in the
927927
// SSR bundle even though the render graph also reaches them.
928928
const serverBundle = readFileSync(path.join(exampleDir, 'dist/server/server.js'), 'utf-8');
929+
const builtHandler = await import(
930+
pathToFileURL(path.join(exampleDir, 'dist/server/server.js')).href + `?fetchable=${Date.now()}`
931+
);
932+
record(
933+
mode,
934+
'build',
935+
'server handler exposes a default Fetchable entry',
936+
typeof builtHandler.default?.fetch === 'function',
937+
);
938+
const fetchableResponse = await builtHandler.default.fetch(
939+
new Request(origin + '/'),
940+
// Fetch hosts commonly pass bindings and execution context after the
941+
// request. The wrapper must ignore them rather than forwarding this
942+
// object as handleRequest's Solid options bag.
943+
{ clientEntry: '/provider-argument-must-not-be-forwarded.js' },
944+
);
945+
const fetchableHtml = await fetchableResponse.text();
946+
record(
947+
mode,
948+
'build',
949+
'default Fetchable ignores provider arguments',
950+
fetchableResponse.status === 200 &&
951+
fetchableHtml.includes('SSR Start Mode') &&
952+
!fetchableHtml.includes('provider-argument-must-not-be-forwarded'),
953+
);
929954
record(
930955
mode,
931956
'build',
@@ -2843,6 +2868,15 @@ async function runDetectMode() {
28432868
'probe ssr environment is non-runnable',
28442869
!isRunnableDevEnvironment(server.environments.ssr),
28452870
);
2871+
const ssrInput = server.environments.ssr.config.build.rollupOptions.input;
2872+
record(
2873+
mode,
2874+
'env',
2875+
'provider environment sees the Fetchable index service entry',
2876+
!!ssrInput &&
2877+
typeof ssrInput === 'object' &&
2878+
ssrInput.index === 'virtual:solid-ssr-handler',
2879+
);
28462880

28472881
httpServer = http.createServer(server.middlewares);
28482882
await new Promise((resolve) => httpServer.listen(0, '127.0.0.1', resolve));

src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,10 @@ export interface Options {
267267
* `dist/client`, server to `dist/server` via the environments/builder
268268
* API). The server bundle's entry is `virtual:solid-ssr-handler`, whose
269269
* `handleRequest(request)` export maps a web `Request` to a streamed
270-
* `Response` — mount it on any server or adapter in one line.
270+
* `Response`; its default `{ fetch(request) }` export provides the same
271+
* handler in the Fetchable shape used by deployment integrations.
272+
* The normal `ssr` environment exposes it as the `index` service entry
273+
* so provider Vite plugins can supply the runtime and build orchestration.
271274
* - With `serverFunctions` also enabled, the prod handler serves the
272275
* server-function endpoint too (in dev the server-function middleware
273276
* already runs first).

src/ssr/index.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
// - Prod: the plugin configures a full-app build (client + server bundles
1515
// via the Vite 6+ environments/builder API — a single `vite build` builds
1616
// both) whose server entry is `virtual:solid-ssr-handler`: an
17-
// adapter-agnostic `handleRequest(Request) => Promise<Response>` that
18-
// scopes each request with `provideRequestEvent`, streams the render, and
19-
// resolves hashed client assets through `virtual:solid-manifest`.
17+
// adapter-agnostic named `handleRequest(Request) => Promise<Response>` plus
18+
// a default Fetchable `{ fetch(request) }` export. Both scope each request
19+
// with `provideRequestEvent`, stream the render, and resolve hashed client
20+
// assets through `virtual:solid-manifest`.
2021
// - Entries are conventional with escape hatches: `src/entry-server.*` /
2122
// `src/entry-client.*` are used when present (or set explicitly); when
2223
// absent, default entries are generated from a single root component
@@ -193,14 +194,16 @@ export interface StartOptions {
193194
* stands its dev middlewares down (SSR serving and the server-function
194195
* endpoint); the generated `virtual:solid-ssr-handler` self-serves
195196
* instead, inlining dev styles through a virtual module and composing the
196-
* server-function endpoint, so `handleRequest(request)` is the whole
197-
* contract in dev exactly as in production. Generated entries and the
198-
* client manifest are still provided.
197+
* server-function endpoint. Its named `handleRequest(request)` and default
198+
* Fetchable exports provide the same contract in dev and production.
199+
* Generated entries and the client manifest are still provided.
199200
*
200201
* Often unnecessary: a provider-owned (non-runnable) `ssr` dev environment
201-
* is detected automatically and the middlewares stand down on their own.
202-
* Set this when the host also owns the server build, or when its
203-
* environment uses a different name so detection can't see it. To hand
202+
* is detected automatically and the middlewares stand down on their own;
203+
* the normal `ssr` environment also exposes the handler as an `index`
204+
* service entry for provider build orchestrators. Set this only when the
205+
* host does not adopt that environment — for example, when it uses a
206+
* different name or independently configures the server build. To hand
204207
* over only the server-function endpoint, use
205208
* `serverFunctions.devMiddleware: false` instead.
206209
*
@@ -863,6 +866,15 @@ export function startServe(
863866
// createSSRResponse committed and pass through untouched.
864867
` return commitEventResponse(response, event);`,
865868
`}`,
869+
``,
870+
`export default {`,
871+
` fetch(request) {`,
872+
// Hosts may pass environment/context arguments after the request.
873+
// Do not alias fetch directly to handleRequest: its second argument is
874+
// the Solid handler options bag, not a provider binding object.
875+
` return handleRequest(request);`,
876+
` },`,
877+
`};`,
866878
);
867879

868880
return lines.join('\n');
@@ -952,9 +964,16 @@ export function startServe(
952964
},
953965
},
954966
ssr: {
967+
consumer: 'server',
955968
build: {
956969
outDir: 'dist/server',
957-
rollupOptions: { input: { server: HANDLER_ID } },
970+
rollupOptions: {
971+
// `index` is the Vite service convention consumed
972+
// by provider orchestrators such as Nitro. Keep the
973+
// standalone artifact's established filename.
974+
input: { index: HANDLER_ID },
975+
output: { entryFileNames: 'server.js' },
976+
},
958977
},
959978
},
960979
},
@@ -965,6 +984,24 @@ export function startServe(
965984
...(env.isSsrBuild ? {} : { builder: {} }),
966985
}
967986
: {
987+
...(!clientMode && !externalServer
988+
? {
989+
environments: {
990+
ssr: {
991+
consumer: 'server' as const,
992+
build: {
993+
outDir: 'dist/server',
994+
rollupOptions: {
995+
// Expose the same service entry during serve so
996+
// provider runtimes can discover and own it.
997+
input: { index: HANDLER_ID },
998+
output: { entryFileNames: 'server.js' },
999+
},
1000+
},
1001+
},
1002+
},
1003+
}
1004+
: {}),
9681005
optimizeDeps: { entries: scanEntries },
9691006
}),
9701007
};

virtual-solid-manifest.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,10 @@ declare module "virtual:solid-ssr-handler" {
4343
serverFunctions?: Record<string, unknown>;
4444
},
4545
): Promise<Response>;
46+
47+
/** Fetchable entry for runtimes and deployment integrations that use the web-standard convention. */
48+
const handler: {
49+
fetch(request: Request): Promise<Response>;
50+
};
51+
export default handler;
4652
}

0 commit comments

Comments
 (0)