diff --git a/.changeset/server-functions-host-environments.md b/.changeset/server-functions-host-environments.md new file mode 100644 index 00000000..cc3e836d --- /dev/null +++ b/.changeset/server-functions-host-environments.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +In dev, the server function manifest is now invalidated in every Vite environment, not just `client` and `ssr`. A host that imports the generated handler from a server environment of its own, such as Void's `void_worker` or a Cloudflare worker, kept a stale manifest before. A server function that only the client referenced then failed with "not part of the deployment" until the dev server restarted. diff --git a/src/server-functions/environments.test.ts b/src/server-functions/environments.test.ts new file mode 100644 index 00000000..2987ac57 --- /dev/null +++ b/src/server-functions/environments.test.ts @@ -0,0 +1,63 @@ +import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createServer, type ViteDevServer } from 'vite'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { serverFunctions } from './index.js'; + +const MANIFEST_ID = 'virtual:solid-server-function-manifest'; + +// A host can own the server side under its own environment name (Void's +// `void_worker`, a Cloudflare worker) and import the manifest there. A server +// function the client reaches first has to show up in that environment's +// manifest too. +describe('server function manifest in a host-named server environment', () => { + let root: string; + let server: ViteDevServer; + + async function loadManifest() { + const result = await server.environments.worker!.transformRequest(MANIFEST_ID); + return result?.code ?? ''; + } + + beforeAll(async () => { + root = await realpath(await mkdtemp(path.join(tmpdir(), 'solid-vite-plugin-env-'))); + await symlink( + fileURLToPath(new URL('../../node_modules', import.meta.url)), + path.join(root, 'node_modules'), + 'dir', + ); + await mkdir(path.join(root, 'src')); + await writeFile( + path.join(root, 'src/greet.ts'), + '"use server";\nexport async function greet(name: string) {\n return `hello ${name}`;\n}\n', + ); + server = await createServer({ + root, + configFile: false, + appType: 'custom', + logLevel: 'silent', + server: { middlewareMode: true, hmr: false, watch: null }, + optimizeDeps: { noDiscovery: true, include: [] }, + environments: { + client: {}, + worker: { consumer: 'server' }, + }, + plugins: [serverFunctions()], + }); + }); + + afterAll(async () => { + await server?.close(); + await rm(root, { recursive: true, force: true }); + }); + + it('invalidates the manifest in that environment when the client finds a new module', async () => { + expect(await loadManifest()).not.toContain('/src/greet.ts'); + + await server.environments.client.transformRequest('/src/greet.ts'); + + expect(await loadManifest()).toContain('"/src/greet.ts"'); + }); +}); diff --git a/src/server-functions/index.ts b/src/server-functions/index.ts index 5bee2d35..ca2cb960 100644 --- a/src/server-functions/index.ts +++ b/src/server-functions/index.ts @@ -347,8 +347,13 @@ function invalidateModules( manifest: string, ): void { if (server?.environments && result.invalidPreload) { - invalidateModule(server.environments.client.moduleGraph, manifest); - invalidateModule(server.environments.ssr.moduleGraph, manifest); + // Every environment, not just `client` and `ssr`: a host can load the + // manifest from a server environment of its own (`void_worker`, a + // Cloudflare worker), and one with no `ssr` environment at all would + // otherwise throw here. + for (const environment of Object.values(server.environments)) { + invalidateModule(environment.moduleGraph, manifest); + } } }