From 4cdb3afe3e5ed56637ed6ca08481cb26caa7b658 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Sat, 22 Aug 2026 19:38:53 -0500 Subject: [PATCH] fix: do not enable devtools from Vite's optional-peer-dep stub resolveDevtools falls back to resolving @solidjs/start-devtools from the plugin's own file so pnpm-isolated installs work, but the package is an optional peer dependency of the plugin, so when it is absent Vite returns its __vite-optional-peer-dep: stub rather than null. The stub was taken as a successful resolution and the generated client entry imported DevToolbar from an empty module, breaking every app without devtools. Treat the stub as not installed. Co-Authored-By: Claude Fable 5 --- .changeset/devtools-optional-peer-stub.md | 5 +++++ src/ssr/index.ts | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 .changeset/devtools-optional-peer-stub.md diff --git a/.changeset/devtools-optional-peer-stub.md b/.changeset/devtools-optional-peer-stub.md new file mode 100644 index 0000000..dbb97c7 --- /dev/null +++ b/.changeset/devtools-optional-peer-stub.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +Start devtools are no longer enabled when `@solidjs/start-devtools` is not installed. Detection falls back to resolving the package from the plugin's own file (for pnpm-isolated installs where it is only a dependency of the plugin), but the package is declared an optional peer dependency of the plugin, so when it is absent Vite answers that resolution with its `__vite-optional-peer-dep:` stub instead of `null`. The plugin took the stub as a successful resolution, wrapped the generated client entry in `DevToolbar`, and the browser failed with `The requested module '/@id/__vite-optional-peer-dep:@solidjs/start-devtools:@solidjs/vite-plugin' does not provide an export named 'DevToolbar'`. The stub is now treated as "not installed". diff --git a/src/ssr/index.ts b/src/ssr/index.ts index bfdcb5d..ed308b5 100644 --- a/src/ssr/index.ts +++ b/src/ssr/index.ts @@ -489,11 +489,18 @@ export function startServe( // from the plugin's own file: in pnpm-isolated apps a copy that is only a // dependency of the plugin is not reachable from the app's importers. The // resolved id is kept so imports from generated modules can use it. - devtoolsResolutions[consumer] ??= (async () => - ( - (await resolve(DEVTOOLS_PACKAGE, importer)) ?? - (await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url))) - )?.id ?? null)(); + devtoolsResolutions[consumer] ??= (async () => { + // Resolving from the plugin's own file never yields null when the + // package is absent: it is declared an optional peer dependency, so + // Vite answers with its `__vite-optional-peer-dep:` stub (an empty + // module). Treat that stub as "not installed". + const realId = (resolved: { id: string } | null) => + resolved && !resolved.id.startsWith('__vite-optional-peer-dep:') ? resolved.id : null; + return ( + realId(await resolve(DEVTOOLS_PACKAGE, importer)) ?? + realId(await resolve(DEVTOOLS_PACKAGE, fileURLToPath(import.meta.url))) + ); + })(); const id = await devtoolsResolutions[consumer]; devtoolsIds[consumer] = id; if (!id && options.devtools === true) {