From b50c0649260ddc3dc00e18c0bc893e3fb30da35e Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 17 Sep 2026 20:12:40 -0500 Subject: [PATCH] fix: keep $ServerOnly templates in client HMR transforms babel-plugin-jsx-dom-expressions 0.40.10 defaults `omitServerOnlyTemplates` to true, so a `$ServerOnly` element compiles to `getNextElement()` with no template. That only works during hydration; Solid Refresh re-runs the component outside of hydration on every hot update, which then throws `TypeError: template is not a function` and leaves stale DOM. Default `omitServerOnlyTemplates` to false for client transforms while HMR is active (dev server, non-production mode, `hot` not disabled). Server and production transforms keep omitting the template, and an explicit `solid.omitServerOnlyTemplates` setting still wins. Fixes #308 Co-Authored-By: Claude Fable 5.1 --- .changeset/server-only-templates-hmr.md | 5 +++++ README.md | 2 ++ src/index.ts | 26 ++++++++++++++++++++++++- 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 .changeset/server-only-templates-hmr.md diff --git a/.changeset/server-only-templates-hmr.md b/.changeset/server-only-templates-hmr.md new file mode 100644 index 0000000..1409096 --- /dev/null +++ b/.changeset/server-only-templates-hmr.md @@ -0,0 +1,5 @@ +--- +'vite-plugin-solid': patch +--- + +Keep `$ServerOnly` templates in client transforms while HMR is active so hot updates of components using `$ServerOnly` no longer throw `template is not a function`. An explicit `solid.omitServerOnlyTemplates` setting still takes precedence. diff --git a/README.md b/README.md index b7b1095..6b21930 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,8 @@ Pass any additional [babel transform options](https://babeljs.io/docs/en/options Pass any additional [babel-plugin-jsx-dom-expressions](https://github.com/ryansolid/dom-expressions/tree/main/packages/babel-plugin-jsx-dom-expressions#plugin-options). They will be merged with the defaults sets by [babel-preset-solid](https://github.com/solidjs/solid/blob/main/packages/babel-preset-solid/index.js#L8-L25). +In dev with HMR enabled, the plugin sets `omitServerOnlyTemplates: false` for client transforms so that components containing `$ServerOnly` elements can be re-created by Solid Refresh. Pass `omitServerOnlyTemplates` explicitly to override this. + #### options.typescript - Type: [@babel/preset-typescript](https://babeljs.io/docs/en/babel-preset-typescript) diff --git a/src/index.ts b/src/index.ts index ead2980..8277024 100644 --- a/src/index.ts +++ b/src/index.ts @@ -105,6 +105,19 @@ export interface Options { */ omitQuotes?: boolean; + /** + * Only applies when `hydratable` is `true`. When `true`, an element marked + * `$ServerOnly` registers no client template, so it can only be obtained by + * hydrating server-rendered DOM and throws if it is ever re-created on the client. + * + * The plugin sets this to `false` for client transforms in dev while HMR is + * enabled, since Solid Refresh re-runs components outside of hydration. Pass + * an explicit value to override that. + * + * @default true + */ + omitServerOnlyTemplates?: boolean; + /** * The name of the runtime module to import the methods from. * @@ -397,11 +410,22 @@ export default function solidPlugin(options: Partial = {}): Plugin { plugins.push('typescript'); } + // babel-plugin-jsx-dom-expressions >= 0.40.10 omits the client template of + // `$ServerOnly` elements by default, so re-creating such an element outside + // of hydration throws "template is not a function". Solid Refresh does + // exactly that on every hot update, so keep the templates in client HMR + // transforms. An explicit user setting still wins. + const presetOptions = { + ...solidOptions, + ...(needHmr && !isSsr ? { omitServerOnlyTemplates: false } : {}), + ...(options.solid || {}), + }; + const opts: babel.TransformOptions = { root: projectRoot, filename: id, sourceFileName: id, - presets: [[solid, { ...solidOptions, ...(options.solid || {}) }]], + presets: [[solid, presetOptions]], plugins: needHmr && !isSsr && !inNodeModules ? [[solidRefresh, { bundler: 'vite' }]] : [], ast: false, sourceMaps: true,