Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/server-functions-client-runtime-prebundle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

With `serverFunctions` on, the dev server now pre-bundles the server-function client runtime even without `components` (`@solidjs/web/server-functions`, or `runtime.client` when it names a package), so a cold cache no longer re-optimizes and reloads on the first `"use server"` module. In Vitest browser mode that reload showed up as "Vite unexpectedly reloaded a test".
109 changes: 108 additions & 1 deletion examples/start-ssr/test/components-warning.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
// derived serverComponents flag) matches `true` exactly, and stays off
// when the option is off,
// - under full SSR start mode `'external'` is redundant but harmless:
// silent, exactly like `true`.
// silent, exactly like `true`,
// - independent of server components: `serverFunctions` alone (with or
// without `ssr`) pre-bundles the server-function client runtime under
// `serve` (the dependency scan never runs the transform that adds its
// import), listed once when components also ask for it; not without
// `serverFunctions`, not in build, and a custom `runtime.client` only as
// a package specifier, beside the components entries when both are on.
//
// Pure resolveConfig — no dev server, no browser. Requires the plugin built
// (pnpm build at the repo root). Usage: node test/components-warning.mjs
Expand Down Expand Up @@ -81,6 +87,14 @@ async function resolveWith(solidOptions) {
config.optimizeDeps.include.includes('@solidjs/web/frames') &&
config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
);
const runtimeEntries = config.optimizeDeps.include.filter(
(dep) => dep === '@solidjs/web/server-functions',
);
record(
'enabled: server-function client runtime listed once',
runtimeEntries.length === 1,
config.optimizeDeps.include.join(', '),
);
}

// ---- `'external'` in the identical config: silent, still enabled ---------
Expand Down Expand Up @@ -123,6 +137,84 @@ async function resolveWith(solidOptions) {
config.optimizeDeps.include.includes('@solidjs/web/performance-tracks'),
config.optimizeDeps.include.join(', '),
);
// Compiled server-function references import the client runtime, but the
// dependency scanner never runs the transform that adds that import.
// Without the entry, the first "use server" module the browser loads
// discovers it late: re-optimize and full reload.
record(
'serverFunctions without components: server-function client runtime pre-bundled',
config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}

// ---- serverFunctions without ssr: same entry -----------------------------
{
const { config } = await resolveWith({ serverFunctions: true });
record(
'serverFunctions without ssr: server-function client runtime pre-bundled',
config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}

// ---- serverFunctions off: no server-function runtime pre-bundle -----------
{
const { config } = await resolveWith({ ssr: true });
record(
'serverFunctions off: server-function client runtime not pre-bundled',
!config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}

// ---- custom runtime: only a package specifier is pre-bundled --------------
{
const client = '@acme/server-runtime/client';
const { config } = await resolveWith({
ssr: true,
serverFunctions: { runtime: { server: '@acme/server-runtime/server', client } },
});
record(
'custom runtime (package): runtime.client pre-bundled instead of the default',
config.optimizeDeps.include.includes(client) &&
!config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}
{
const client = '@acme/server-runtime/client';
const { config } = await resolveWith({
ssr: true,
serverFunctions: {
components: true,
runtime: { server: '@acme/server-runtime/server', client },
},
});
record(
'components + custom runtime (package): runtime.client beside the components entries',
config.optimizeDeps.include.includes(client) &&
config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}
for (const client of [
'./src/sf-client.ts',
'/src/sf-client.ts',
'virtual:sf-client',
'@/sf-client',
'~/sf-client',
]) {
const { config } = await resolveWith({
ssr: true,
serverFunctions: { runtime: { server: client, client } },
});
record(
`custom runtime (${client}): neither it nor the default pre-bundled`,
!config.optimizeDeps.include.includes(client) &&
!config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}

// ---- build: optimizeDeps is serve-only, the entries stay out --------------
Expand All @@ -138,6 +230,21 @@ async function resolveWith(solidOptions) {
config.optimizeDeps.include.join(', '),
);
}
{
const config = await resolveConfig(
{
root: exampleDir,
configFile: false,
plugins: [solidPlugin({ ssr: true, serverFunctions: true })],
},
'build',
);
record(
'build: server-function client runtime not in optimizeDeps.include',
!config.optimizeDeps.include.includes('@solidjs/web/server-functions'),
config.optimizeDeps.include.join(', '),
);
}

// ---- full SSR start mode: `'external'` is redundant but harmless ---------
{
Expand Down
36 changes: 36 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,18 @@ function findPackageDir(name: string, root: string): string | undefined {
}
}

/**
* Whether `id` looks like a package specifier (`name`, `name/sub`,
* `@scope/name/sub`), by syntax alone. Relative and absolute paths, ids with a
* protocol or drive (`virtual:`, `node:`, `C:`), subpath imports (`#x`) and
* aliases like `~/x` or `@/x` don't.
*/
function isPackageSpecifier(id: string): boolean {
if (!id || /^[./\\#~\0]/.test(id) || id.includes(':')) return false;
if (id.startsWith('@')) return /^@[^/]+\/[^/]/.test(id);
return true;
}

function getJestDomExport(setupFiles: string[], root: string) {
if (setupFiles?.some((file) => /jest-dom/.test(file))) return undefined;

Expand Down Expand Up @@ -1057,6 +1069,21 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
const serverComponentsOption =
typeof options.serverFunctions === 'object' ? options.serverFunctions.components : undefined;
const serverComponents = !!serverComponentsOption;
// The client runtime compiled server-function references import (the
// server-functions plugin's own default unless `runtime` is set), kept only
// when it names a package. A relative path or an alias is app source: the
// optimizer would bundle a copy nothing loads or, for `@/x`, serve that copy
// so edits stop showing up. A virtual id or `/src/x` doesn't resolve for
// `include` and only warns.
const serverFunctionsClientRuntime = options.serverFunctions
? typeof options.serverFunctions === 'object' && options.serverFunctions.runtime
? options.serverFunctions.runtime.client
: '@solidjs/web/server-functions'
: undefined;
const serverFunctionsClientDep =
serverFunctionsClientRuntime && isPackageSpecifier(serverFunctionsClientRuntime)
? serverFunctionsClientRuntime
: undefined;
// `start: true` is sugar for the empty options bag — one start mode,
// two spellings — so normalize here and let everything downstream see a
// single shape (`false` behaves exactly like omission).
Expand Down Expand Up @@ -1483,6 +1510,15 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
...(command === 'serve' && serverComponents
? ['@solidjs/web/frames', '@solidjs/web/server-functions']
: []),
// Plain `serverFunctions` needs the runtime too: the scanner never
// runs the transform that adds its import, so the first "use server"
// module forces the same re-optimize + full reload (in Vitest
// browser mode, "Vite unexpectedly reloaded a test").
...(command === 'serve' &&
serverFunctionsClientDep &&
!(serverComponents && serverFunctionsClientDep === '@solidjs/web/server-functions')
? [serverFunctionsClientDep]
: []),
// The attribution engine and the Chrome performance-tracks
// recorder are subpaths the scanner only sees when the app's own
// graph imports them; a consumer it never crawls (a linked
Expand Down