Skip to content

Commit 18a67d4

Browse files
committed
move server fns to dismantle
1 parent 04b5a7c commit 18a67d4

11 files changed

Lines changed: 958 additions & 667 deletions

File tree

apps/tests/src/entry-server.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ export default createHandler(() => (
1919
)}
2020
/>
2121
));
22+
23+
24+
import 'solid-start/fns/preload';

packages/start/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@babel/traverse": "^7.28.3",
4242
"@babel/types": "^7.28.5",
4343
"@solidjs/meta": "^0.29.4",
44-
"@tanstack/server-functions-plugin": "1.134.5",
44+
"dismantle": "^0.6.0",
4545
"@types/babel__traverse": "^7.28.0",
4646
"@types/micromatch": "^4.0.9",
4747
"cookie-es": "^2.0.0",

packages/start/src/config/index.ts

Lines changed: 3 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { TanStackServerFnPlugin } from "@tanstack/server-functions-plugin";
1+
22
import { defu } from "defu";
33
import { globSync } from "node:fs";
44
import { extname, isAbsolute, join } from "node:path";
5-
import { fileURLToPath } from "node:url";
65
import { normalizePath, type PluginOption } from "vite";
76
import solid, { type Options as SolidOptions } from "vite-plugin-solid";
87

@@ -14,6 +13,7 @@ import type { BaseFileSystemRouter } from "./fs-routes/router.ts";
1413
import lazy from "./lazy.ts";
1514
import { manifest } from "./manifest.ts";
1615
import { parseIdQuery } from "./utils.ts";
16+
import { serverFunctionsPlugin } from "./server-functions.ts";
1717

1818
export interface SolidStartOptions {
1919
solid?: Partial<SolidOptions>;
@@ -163,42 +163,9 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
163163
},
164164
}),
165165
lazy(),
166+
serverFunctionsPlugin({}),
166167
// Must be placed after fsRoutes, as treeShake will remove the
167168
// server fn exports added in by this plugin
168-
TanStackServerFnPlugin({
169-
// This is the ID that will be available to look up and import
170-
// our server function manifest and resolve its module
171-
manifestVirtualImportId: VIRTUAL_MODULES.serverFnManifest,
172-
directive: "use server",
173-
callers: [
174-
{
175-
envConsumer: "client",
176-
envName: VITE_ENVIRONMENTS.client,
177-
getRuntimeCode: () =>
178-
`import { createServerReference } from "${normalizePath(
179-
fileURLToPath(new URL("../server/server-runtime", import.meta.url))
180-
)}"`,
181-
replacer: opts => `createServerReference('${opts.functionId}')`,
182-
},
183-
{
184-
envConsumer: "server",
185-
envName: VITE_ENVIRONMENTS.server,
186-
getRuntimeCode: () =>
187-
`import { createServerReference } from '${normalizePath(
188-
fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url))
189-
)}'`,
190-
replacer: opts => `createServerReference(${opts.fn}, '${opts.functionId}')`,
191-
},
192-
],
193-
provider: {
194-
envName: VITE_ENVIRONMENTS.server,
195-
getRuntimeCode: () =>
196-
`import { createServerReference } from '${normalizePath(
197-
fileURLToPath(new URL("../server/server-fns-runtime", import.meta.url))
198-
)}'`,
199-
replacer: opts => `createServerReference(${opts.fn}, '${opts.functionId}')`,
200-
},
201-
}),
202169
{
203170
name: "solid-start:virtual-modules",
204171
async resolveId(id) {
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
import * as dismantle from "dismantle";
2+
import { fileURLToPath } from "node:url";
3+
import {
4+
createFilter,
5+
normalizePath,
6+
type FilterPattern,
7+
type Plugin,
8+
type ViteDevServer,
9+
} from "vite";
10+
11+
type CompileOptions = Pick<dismantle.Options, "mode" | "env">;
12+
13+
type CompileOutput = dismantle.Output;
14+
15+
const CLIENT = normalizePath(fileURLToPath(new URL("../fns/client", import.meta.url)));
16+
const SERVER = normalizePath(fileURLToPath(new URL("../fns/server", import.meta.url)));
17+
const RUNTIME = normalizePath(fileURLToPath(new URL("../fns/runtime", import.meta.url)));
18+
19+
async function compile(
20+
id: string,
21+
code: string,
22+
options: CompileOptions,
23+
): Promise<CompileOutput> {
24+
return await dismantle.compile(id, code, {
25+
runtime: RUNTIME,
26+
key: "solid-start",
27+
mode: options.mode,
28+
env: options.env,
29+
definitions: [
30+
{
31+
type: "function-directive",
32+
directive: "use server",
33+
pure: true,
34+
// Where to instanciate the handlers
35+
target: {
36+
kind: "named",
37+
name: "createServerReference",
38+
source:
39+
options.mode === "client"
40+
? CLIENT
41+
: SERVER,
42+
},
43+
/**
44+
* A resolver for the replaced functions
45+
*
46+
* dismantle will output something like this:
47+
* const foo = handler(async () => {
48+
* const mod = (await import(modulePath)).default;
49+
* return (...args) => {
50+
* // ...
51+
* };
52+
* });
53+
*/
54+
handle: {
55+
kind: "named",
56+
name: "createServerFunction",
57+
source:
58+
options.mode === "client"
59+
? CLIENT
60+
: SERVER,
61+
},
62+
// isomorphic: true,
63+
},
64+
],
65+
});
66+
}
67+
68+
interface ManifestRecord {
69+
files: CompileOutput["files"];
70+
entries: Set<string>;
71+
}
72+
73+
type Manifest = Record<CompileOptions["mode"], ManifestRecord>;
74+
75+
function createManifest(): Manifest {
76+
return {
77+
server: {
78+
files: new Map(),
79+
entries: new Set(),
80+
},
81+
client: {
82+
files: new Map(),
83+
entries: new Set(),
84+
},
85+
};
86+
}
87+
88+
function mergeManifestRecord(
89+
source: ManifestRecord,
90+
target: ManifestRecord,
91+
): { invalidePreload: boolean; invalidated: string[] } {
92+
const invalidated: string[] = [];
93+
for (const [file, content] of target.files) {
94+
if (source.files.has(file)) {
95+
invalidated.push(file);
96+
}
97+
source.files.set(file, content);
98+
}
99+
100+
const current = source.entries.size;
101+
for (const entry of target.entries) {
102+
source.entries.add(entry);
103+
}
104+
return {
105+
invalidePreload: current !== source.entries.size,
106+
invalidated,
107+
};
108+
}
109+
110+
export interface ServerFunctionsFilter {
111+
include?: FilterPattern;
112+
exclude?: FilterPattern;
113+
}
114+
115+
export interface ServerFunctionsOptions {
116+
filter?: ServerFunctionsFilter;
117+
}
118+
119+
const DEFAULT_INCLUDE = "src/**/*.{jsx,tsx,ts,js,mjs,cjs}";
120+
const DEFAULT_EXCLUDE = "node_modules/**/*.{jsx,tsx,ts,js,mjs,cjs}";
121+
122+
const VIRTUAL_MODULE = "solid-start/fns/preload";
123+
124+
interface DeferredPromise<T> {
125+
reference: Promise<T>;
126+
resolve: (value: T) => void;
127+
reject: (value: any) => void;
128+
}
129+
130+
function createDeferredPromise<T>(): DeferredPromise<T> {
131+
let resolve: DeferredPromise<T>["resolve"];
132+
let reject: DeferredPromise<T>["reject"];
133+
134+
return {
135+
reference: new Promise((res, rej) => {
136+
resolve = res;
137+
reject = rej;
138+
}),
139+
resolve(value) {
140+
resolve(value);
141+
},
142+
reject(value) {
143+
reject(value);
144+
},
145+
};
146+
}
147+
148+
class Debouncer<T> {
149+
promise: DeferredPromise<T>;
150+
151+
private timeout: ReturnType<typeof setTimeout> | undefined;
152+
153+
constructor(private source: () => T) {
154+
this.promise = createDeferredPromise();
155+
this.defer();
156+
}
157+
158+
defer(): void {
159+
if (this.timeout) {
160+
clearTimeout(this.timeout);
161+
this.timeout = undefined;
162+
}
163+
this.timeout = setTimeout(() => {
164+
this.promise.resolve(this.source());
165+
}, 1000);
166+
}
167+
}
168+
169+
function invalidateModules(
170+
server: ViteDevServer | undefined,
171+
result: ReturnType<typeof mergeManifestRecord>,
172+
): void {
173+
if (server) {
174+
for (let i = 0, len = result.invalidated.length; i < len; i++) {
175+
const invalidated = result.invalidated[i];
176+
if (invalidated) {
177+
const target = server.moduleGraph.getModuleById(invalidated);
178+
if (target) {
179+
server.moduleGraph.invalidateModule(target);
180+
}
181+
}
182+
}
183+
if (result.invalidePreload) {
184+
const target = server.moduleGraph.getModuleById(VIRTUAL_MODULE);
185+
if (target) {
186+
server.moduleGraph.invalidateModule(target);
187+
}
188+
}
189+
}
190+
}
191+
192+
export function serverFunctionsPlugin(
193+
options: ServerFunctionsOptions,
194+
): Plugin[] {
195+
const filter = createFilter(
196+
options.filter?.include || DEFAULT_INCLUDE,
197+
options.filter?.exclude || DEFAULT_EXCLUDE,
198+
);
199+
200+
let env: CompileOptions["env"];
201+
202+
const manifest = createManifest();
203+
204+
const preload: Record<
205+
CompileOptions["mode"],
206+
Debouncer<string> | undefined
207+
> = {
208+
server: undefined,
209+
client: undefined,
210+
};
211+
212+
let currentServer: ViteDevServer;
213+
214+
return [
215+
{
216+
name: "solid-start:server-functions/setup",
217+
enforce: "pre",
218+
configResolved(config) {
219+
env = config.mode !== "production" ? "development" : "production";
220+
},
221+
configureServer(server) {
222+
currentServer = server;
223+
},
224+
},
225+
{
226+
name: "solid-start:server-functions/preload",
227+
enforce: "pre",
228+
resolveId(source) {
229+
if (source === VIRTUAL_MODULE) {
230+
return { id: VIRTUAL_MODULE, moduleSideEffects: true };
231+
}
232+
return null;
233+
},
234+
load(id, opts) {
235+
const mode = opts?.ssr ? 'server' : 'client';
236+
if (id === VIRTUAL_MODULE) {
237+
const current = new Debouncer(() =>
238+
[...manifest[mode].entries]
239+
.map(entry => `import "${entry}";`)
240+
.join('\n'),
241+
);
242+
preload[mode] = current;
243+
return current.promise.reference;
244+
}
245+
return null;
246+
},
247+
},
248+
{
249+
name: "solid-start:server-functions/virtuals",
250+
enforce: "pre",
251+
async resolveId(source, importer, opts) {
252+
if (importer) {
253+
const result = await this.resolve(source, importer, opts);
254+
const mode = opts?.ssr ? 'server' : 'client';
255+
if (result && manifest[mode].files.has(result.id)) {
256+
return result;
257+
}
258+
}
259+
return null;
260+
},
261+
load(id, opts) {
262+
const mode = opts?.ssr ? 'server' : 'client';
263+
const result = manifest[mode].files.get(id);
264+
if (result) {
265+
return {
266+
code: result.code || '',
267+
map: result.map,
268+
};
269+
}
270+
return null;
271+
},
272+
},
273+
{
274+
name: "solid-start:server-functions/compiler",
275+
async transform(code, id, opts) {
276+
const mode = opts?.ssr ? 'server' : 'client';
277+
if (!filter(id)) {
278+
return null;
279+
}
280+
const preloader = preload[mode];
281+
if (preloader) {
282+
preloader.defer();
283+
}
284+
const result = await compile(id, code, {
285+
...options,
286+
mode,
287+
env,
288+
});
289+
290+
invalidateModules(
291+
currentServer,
292+
mergeManifestRecord(manifest[mode], {
293+
files: result.files,
294+
entries: new Set(result.entries),
295+
}),
296+
);
297+
298+
return {
299+
code: result.code || '',
300+
map: result.map,
301+
};
302+
},
303+
},
304+
];
305+
}

0 commit comments

Comments
 (0)