From 9fbbef6bd1115c54a871e5f8ccbbbe3c7652f6ce Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Sat, 5 Sep 2026 01:35:13 -0700 Subject: [PATCH] Record client-reachable server function ids in the persisted manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dist/client/.vite/solid-server-functions.json grows from a module list to { modules, functions: [{ id, name, module }] }: every server function the client compile emitted a reference to, by wire id. Build tooling that needs the client-reachable set — a static-site prerenderer verifying each reachable function was captured — reads it here instead of re-deriving it from compiled output. The legacy array shape is still accepted on read; the type is exported as PersistedServerFunctionManifest. Co-authored-by: Cursor --- .changeset/server-function-manifest-ids.md | 5 ++ src/index.ts | 5 +- src/server-functions/index.ts | 89 +++++++++++++++++----- 3 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 .changeset/server-function-manifest-ids.md diff --git a/.changeset/server-function-manifest-ids.md b/.changeset/server-function-manifest-ids.md new file mode 100644 index 0000000..90569c0 --- /dev/null +++ b/.changeset/server-function-manifest-ids.md @@ -0,0 +1,5 @@ +--- +"@solidjs/vite-plugin": patch +--- + +The persisted server-function manifest (`dist/client/.vite/solid-server-functions.json`) now records every server function the client build can reach, by wire id, alongside the module list: `{ modules: string[], functions: Array<{ id, name, module }> }`. Build tooling that needs the client-reachable set — a static-site prerenderer verifying that each reachable function was captured at build time, for example — reads it from here instead of re-deriving it from compiled output. The previous array shape is still accepted when read (the type is exported as `PersistedServerFunctionManifest`). diff --git a/src/index.ts b/src/index.ts index 7b316a1..1206628 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,10 @@ import { export { devStylePatch } from './dev-manifest.js'; export { serverFunctions }; export type { ServerFunctionsOptions }; -export type { ServerFunctionsFilter } from './server-functions/index.js'; +export type { + PersistedServerFunctionManifest, + ServerFunctionsFilter, +} from './server-functions/index.js'; export type { StartOptions }; import path from 'path'; import type { FilterPattern, Plugin, ViteDevServer } from 'vite'; diff --git a/src/server-functions/index.ts b/src/server-functions/index.ts index 0a081ad..e51701b 100644 --- a/src/server-functions/index.ts +++ b/src/server-functions/index.ts @@ -183,13 +183,30 @@ const HANDLER_ID = 'virtual:solid-server-function-handler'; // (`vite build` then `vite build --ssr`) does not, so the client build // persists its findings for the SSR build to merge (mirroring the plugin's // dist/client/.vite/manifest.json convention). +// +// The file doubles as the build's statement of which server functions the +// CLIENT can reach — every reference the client compile emitted, by wire id +// — for build tooling that needs that set without re-deriving it from +// compiled output (a static-site prerenderer checking that each reachable +// function was captured, for example). Paths are root-relative, posix. const PERSISTED_MANIFEST_PATH = '.vite/solid-server-functions.json'; +/** The persisted manifest's on-disk shape (the array form is the pre-`functions` legacy). */ +export interface PersistedServerFunctionManifest { + /** Modules containing server functions, root-relative. */ + modules: string[]; + /** Every server function the client build emitted a reference to. */ + functions: Array<{ id: string; name: string; module: string }>; +} + function readPersistedManifest(root: string): Set { const file = path.resolve(root, 'dist/client', PERSISTED_MANIFEST_PATH); if (!existsSync(file)) return new Set(); try { - const entries: string[] = JSON.parse(readFileSync(file, 'utf-8')); + const parsed: string[] | PersistedServerFunctionManifest = JSON.parse( + readFileSync(file, 'utf-8'), + ); + const entries = Array.isArray(parsed) ? parsed : parsed.modules; return new Set( entries.map((entry) => path.resolve(root, entry)).filter((entry) => existsSync(entry)), ); @@ -198,21 +215,46 @@ function readPersistedManifest(root: string): Set { } } -function writePersistedManifest(root: string, outDir: string, entries: Set): void { +function writePersistedManifest( + root: string, + outDir: string, + entries: Set, + functions: Map, +): void { const file = path.resolve(root, outDir, PERSISTED_MANIFEST_PATH); mkdirSync(path.dirname(file), { recursive: true }); - const relative = [...entries].map((entry) => - path.relative(root, entry).split(path.sep).join('/'), - ); - writeFileSync(file, JSON.stringify(relative, null, 2)); + const relative = (entry: string) => path.relative(root, entry).split(path.sep).join('/'); + const manifest: PersistedServerFunctionManifest = { + modules: [...entries].map(relative), + functions: [...functions].map(([id, record]) => ({ + id, + name: record.name, + module: relative(record.module), + })), + }; + writeFileSync(file, JSON.stringify(manifest, null, 2)); +} + +interface FunctionRecord { + name: string; + /** Absolute path of the declaring module. */ + module: string; } -type Manifest = Record>; +interface Manifest { + /** Modules with server functions, per consumer. */ + modules: Record>; + /** Wire id -> declaring function, for every reference the CLIENT compile emitted. */ + clientFunctions: Map; +} function createManifest(): Manifest { return { - server: new Set(), - client: new Set(), + modules: { + server: new Set(), + client: new Set(), + }, + clientFunctions: new Map(), }; } @@ -477,13 +519,13 @@ export function serverFunctions( const hashIndex = new Map(); let hashIndexSize = -1; function moduleForFunctionId(functionId: string): string | undefined { - if (manifest.server.size !== hashIndexSize) { + if (manifest.modules.server.size !== hashIndexSize) { hashIndex.clear(); - for (const entry of manifest.server) { + for (const entry of manifest.modules.server) { const relative = path.relative(root, entry).split(path.sep).join('/'); hashIndex.set(xxHash32(relative).toString(16), entry); } - hashIndexSize = manifest.server.size; + hashIndexSize = manifest.modules.server.size; } return hashIndex.get(functionId.split('-')[1]!); } @@ -628,11 +670,24 @@ export function serverFunctions( if (!result.valid) return null; + // The client compile is the authority on what the browser can dispatch: + // record every reference it emitted, by wire id, for the persisted + // manifest. A module is re-transformed on change, so its previous ids + // are dropped first (a renamed function must not linger as reachable). + if (mode === 'client') { + for (const [functionId, record] of manifest.clientFunctions) { + if (record.module === id) manifest.clientFunctions.delete(functionId); + } + for (const fn of result.functions) { + manifest.clientFunctions.set(fn.id, { name: fn.name, module: id }); + } + } + const preloader = preload[mode]; if (preloader) preloader.defer(); invalidateModules( currentServer, - mergeManifestRecord(manifest.server, new Set([id])), + mergeManifestRecord(manifest.modules.server, new Set([id])), manifestId, ); @@ -689,7 +744,7 @@ export function serverFunctions( // build discovered so the server manifest registers them even when // the SSR module graph never imports them. for (const entry of readPersistedManifest(root)) { - manifest.server.add(entry); + manifest.modules.server.add(entry); } } }, @@ -704,7 +759,7 @@ export function serverFunctions( const consumer = ctx.environment?.config?.consumer; const isClient = consumer ? consumer === 'client' : !isSsrBuild; if (isBuild && isClient) { - writePersistedManifest(root, outDir, manifest.server); + writePersistedManifest(root, outDir, manifest.modules.server, manifest.clientFunctions); } }, }, @@ -727,11 +782,11 @@ export function serverFunctions( // configs resolve before the client build has written the file, // but this load runs once the SSR environment builds — after it. for (const entry of readPersistedManifest(root)) { - manifest.server.add(entry); + manifest.modules.server.add(entry); } } const current = new Debouncer(() => - [...manifest[mode]].map((entry) => `import ${JSON.stringify(entry)};`).join('\n'), + [...manifest.modules[mode]].map((entry) => `import ${JSON.stringify(entry)};`).join('\n'), ); preload[mode] = current; const result = await current.promise.reference;