Skip to content

Commit ecc02be

Browse files
authored
fix: resolve unevaluated server functions in dev (#2208)
1 parent 85b24b2 commit ecc02be

4 files changed

Lines changed: 67 additions & 15 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@solidjs/start": patch
3+
---
4+
5+
fix: resolve server functions in dev when their route was reached through client-side navigation before its module was evaluated on the server

packages/start/src/directives/index.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
type Plugin,
66
type ViteDevServer,
77
} from "vite";
8+
import fg from "fast-glob";
89
import { compile, type CompileOptions } from "./compile.ts";
10+
import xxHash32 from "./xxhash32.ts";
911

1012
export interface ServerFunctionsFilter {
1113
include?: FilterPattern;
@@ -25,6 +27,11 @@ const DEFAULT_INCLUDE = "src/**/*.{jsx,tsx,ts,js,mjs,cjs}";
2527
const DEFAULT_EXCLUDE = "node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}";
2628
const DIRECTIVE = "use server";
2729

30+
// Dev-only virtual module used by fns/handler.ts to lazily resolve a server
31+
// function id back to its owning module when the function was never evaluated
32+
// in the server environment (e.g. its route was only client-side navigated to).
33+
const LOOKUP_ID = "solid-start:server-fn-lookup";
34+
2835
type Manifest = Record<CompileOptions["mode"], Set<string>>;
2936

3037
function createManifest(): Manifest {
@@ -107,13 +114,15 @@ function invalidateModules(
107114
): void {
108115
if (server) {
109116
if (result.invalidPreload) {
110-
invalidateModule(server.environments.client.moduleGraph, manifest);
111-
invalidateModule(server.environments.ssr.moduleGraph, manifest);
117+
// Environments are not limited to "client"/"ssr": plugins like nitro
118+
// render in their own environment (e.g. "nitro"), so invalidate everywhere.
119+
for (const environment of Object.values(server.environments)) {
120+
invalidateModule(environment.moduleGraph, manifest);
121+
}
112122
}
113123
}
114124
}
115125

116-
117126
export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[] {
118127
const filter = createFilter(
119128
options.filter?.include || DEFAULT_INCLUDE,
@@ -179,6 +188,9 @@ export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[]
179188
if (source === options.manifest) {
180189
return { id: options.manifest, moduleSideEffects: true };
181190
}
191+
if (source.startsWith(LOOKUP_ID)) {
192+
return { id: source, moduleSideEffects: true };
193+
}
182194
return null;
183195
},
184196
async load(id) {
@@ -191,12 +203,32 @@ export function serverFunctionsPlugin(options: ServerFunctionsOptions): Plugin[]
191203
const result = await current.promise.reference;
192204
return result;
193205
}
206+
if (id.startsWith(LOOKUP_ID)) {
207+
if (this.environment.mode !== "dev") {
208+
throw new Error(`${LOOKUP_ID} is only available in dev`);
209+
}
210+
const functionId = new URLSearchParams(id.slice(LOOKUP_ID.length + 1)).get("id");
211+
// dev function ids are `${xxHash32(file)}-${count}-${name}`
212+
const hash = functionId?.split("-")[0];
213+
if (!hash) return "export {};";
214+
215+
// Look through the files already discovered by the transform first;
216+
// fall back to scanning the project so a function is found even when
217+
// the browser kept a chunk from before a dev server restart.
218+
let files = [...manifest.server].filter(file => xxHash32(file).toString(16) === hash);
219+
if (files.length === 0) {
220+
files = fg
221+
.sync(DEFAULT_INCLUDE, { cwd: this.environment.config.root, absolute: true })
222+
.filter(file => filter(file) && xxHash32(file).toString(16) === hash);
223+
}
224+
return files.map(file => `import ${JSON.stringify(file)};`).join("\n") || "export {};";
225+
}
194226
return null;
195227
},
196228
},
197229
{
198230
name: "solid-start:server-functions/compiler",
199-
enforce: 'pre',
231+
enforce: "pre",
200232
async transform(code, fileId) {
201233
const mode = this.environment.config.consumer;
202234
const [id] = fileId.split("?");

packages/start/src/fns/handler.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,10 @@ import {
1111
serializeToJSONStream,
1212
serializeToJSStream,
1313
} from "./serialization.ts";
14-
import {
15-
BODY_FORMAT_KEY,
16-
BodyFormat,
17-
extractBody,
18-
getHeadersAndBody,
19-
} from "./shared.ts";
14+
import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./shared.ts";
2015
import "solid-start:server-fn-manifest";
2116

22-
import { getServerFunction } from "./registration.ts";
17+
import { getServerFunction, hasServerFunction } from "./registration.ts";
2318
import type { FetchEvent, PageEvent } from "../server/types.ts";
2419
import { getExpectedRedirectStatus } from "../server/util.ts";
2520

@@ -45,6 +40,24 @@ export async function handleServerFunction(h3Event: H3Event) {
4540
}
4641
}
4742

43+
if (import.meta.env.DEV && !hasServerFunction(functionId!)) {
44+
// The module that owns this function has not been evaluated in the server
45+
// environment yet (e.g. the route was reached by client-side navigation, so
46+
// it was only ever loaded in the browser). Ask the dev server to resolve the
47+
// function id back to its owning module and import it, which registers it.
48+
// Resolved by the "solid-start:server-functions/preload" plugin.
49+
try {
50+
// the /@id/ prefix routes the request through the plugin container so the
51+
// virtual module can be resolved (bare ids get node-resolved by the runner)
52+
await import(
53+
/* @vite-ignore */ `/@id/solid-start:server-fn-lookup?id=${encodeURIComponent(functionId!)}`
54+
);
55+
} catch (error) {
56+
// fall through to getServerFunction, which reports the missing function
57+
console.error("[solid-start] server function lookup failed:", error);
58+
}
59+
}
60+
4861
const serverFunction = getServerFunction(functionId!);
4962

5063
let parsed: any[] = [];

packages/start/src/fns/registration.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ export function registerServerFunction<T extends any[], R>(
88
return callback;
99
}
1010

11-
export function getServerFunction<T extends any[], R>(
12-
id: string,
13-
): ((...args: T) => Promise<R>) {
11+
export function hasServerFunction(id: string): boolean {
12+
return REGISTRATIONS.has(id);
13+
}
14+
15+
export function getServerFunction<T extends any[], R>(id: string): (...args: T) => Promise<R> {
1416
const fn = REGISTRATIONS.get(id) as ((...args: T) => Promise<R>) | undefined;
1517
if (fn) {
1618
return fn;
1719
}
18-
throw new Error('invalid server function: ' + id);
20+
throw new Error("invalid server function: " + id);
1921
}

0 commit comments

Comments
 (0)