Skip to content
Closed
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-only-hmr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Fix hot reloading for components whose root element uses `$ServerOnly`. The first edit threw `template is not a function`, because the Solid compiler leaves `$ServerOnly` templates out of the client bundle and hot reloading needs them to re-run the component. In dev, the client build now keeps these templates. Production builds still leave them out. This needs `babel-plugin-jsx-dom-expressions` 0.40.10 or newer, which fresh installs already get; with an older version the behavior is unchanged.
5 changes: 3 additions & 2 deletions packages/start/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defu } from "defu";
import { globSync } from "node:fs";
import { basename, extname, isAbsolute, join } from "node:path";
import type { PluginOption, FilterPattern } from "vite";
import solid, { type Options as SolidOptions } from "vite-plugin-solid";
import type { Options as SolidOptions } from "vite-plugin-solid";
import { type ServerFunctionsOptions, serverFunctionsPlugin } from "../directives/index.ts";
import { appRootAlias } from "./app-root-alias.ts";
import { boundaryModules } from "./boundary-modules.ts";
Expand All @@ -15,6 +15,7 @@ import type { BaseFileSystemRouter } from "./fs-routes/router.ts";
import { sanitizeChunkFileName, toPickId } from "./fs-routes/tree-shake.ts";
import lazy from "./lazy.ts";
import { manifest } from "./manifest.ts";
import { solidPlugins } from "./solid.ts";
import { parseIdQuery } from "./utils.ts";

/**
Expand Down Expand Up @@ -434,7 +435,7 @@ export function solidStart(options?: SolidStartOptions): Array<PluginOption> {
},
},
devServer(handlers.server),
solid({
...solidPlugins({
...start.solid,
ssr: true,
extensions: extensions.map(ext => `.${ext}`),
Expand Down
54 changes: 54 additions & 0 deletions packages/start/src/config/solid.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { Plugin } from "vite";
import { describe, expect, it } from "vitest";
import { solidPlugins } from "./solid.ts";

const SOURCE = `export default function Page() {
return (
<main $ServerOnly>
<p>server only content</p>
</main>
);
}`;

/** Compiles SOURCE for the client, as `vite serve` or `vite build` would. */
async function compileForClient(
command: "serve" | "build",
options: Parameters<typeof solidPlugins>[0] = {},
): Promise<string> {
const plugins = solidPlugins({ ssr: true, ...options }) as Plugin[];
const config = { command, mode: command === "serve" ? "development" : "production" };
for (const plugin of plugins) {
await (plugin.configResolved as any)?.call({}, config);
}
const solid = plugins.find(plugin => plugin.name === "solid")!;
const result = await (solid.transform as any).call(
{},
SOURCE,
`${process.cwd()}/src/routes/page.tsx`,
{ ssr: false },
);
return result.code;
}

describe("solidPlugins", () => {
// Hot reloading re-runs the component on the client, which needs the
// template (https://github.com/solidjs/solid-start/issues/1930).
it("keeps $ServerOnly templates in the client dev build", async () => {
expect(await compileForClient("serve")).toContain("server only content");
});

it("leaves $ServerOnly templates out of the client production build", async () => {
expect(await compileForClient("build")).not.toContain("server only content");
});

it("keeps the value the user sets", async () => {
const code = await compileForClient("serve", {
solid: { omitServerOnlyTemplates: true } as any,
});
expect(code).not.toContain("server only content");
});

it("leaves the templates out when hot reloading is turned off", async () => {
expect(await compileForClient("serve", { hot: false })).not.toContain("server only content");
});
});
42 changes: 42 additions & 0 deletions packages/start/src/config/solid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Plugin, PluginOption } from "vite";
import solid, { type Options as SolidOptions } from "vite-plugin-solid";

type CompilerOptions = NonNullable<SolidOptions["solid"]> & {
/**
* Leave the templates of `$ServerOnly` elements out of client bundles.
* Added in `babel-plugin-jsx-dom-expressions` 0.40.10, and on by default.
*/
omitServerOnlyTemplates?: boolean;
};

/**
* `vite-plugin-solid`, set up so that hot reloading works with `$ServerOnly`.
*
* The Solid compiler leaves the templates of `$ServerOnly` elements out of the
* client bundle, since hydration reuses the server-rendered DOM. Hot module
* replacement re-runs the component on the client and needs them, so without
* them the first edit throws "template is not a function"
* (https://github.com/solidjs/solid-start/issues/1930). In dev, keep them.
* Production builds still leave them out, and a value set by the user is kept.
*/
export function solidPlugins(options: Partial<SolidOptions>): PluginOption[] {
const compilerOptions: CompilerOptions = { ...options.solid };

const serverOnlyHmr: Plugin = {
name: "solid-start:server-only-hmr",
configResolved(config) {
// vite-plugin-solid reads these options on every transform, and uses the
// same condition to decide whether to enable hot reloading.
if (
config.command === "serve" &&
config.mode !== "production" &&
options.hot !== false &&
!("omitServerOnlyTemplates" in compilerOptions)
) {
compilerOptions.omitServerOnlyTemplates = false;
}
},
};

return [serverOnlyHmr, solid({ ...options, solid: compilerOptions })];
}
18 changes: 9 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading