From 1914c40c1d3d3ebe52df559c5f6f29521943f87d Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Wed, 15 Jul 2026 19:54:13 -0500 Subject: [PATCH] fix: reclassify lazy facades without importers --- .changeset/lazy-entry-eliminated-importers.md | 5 +++ examples/vite-8/src/App.tsx | 3 +- examples/vite-8/src/UnusedLazy.tsx | 3 ++ examples/vite-8/src/UnusedLazyImporter.ts | 9 ++++++ examples/vite-8/vite.config.ts | 31 +++++++++++++++++-- src/index.ts | 25 +++++++++++++-- 6 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 .changeset/lazy-entry-eliminated-importers.md create mode 100644 examples/vite-8/src/UnusedLazy.tsx create mode 100644 examples/vite-8/src/UnusedLazyImporter.ts diff --git a/.changeset/lazy-entry-eliminated-importers.md b/.changeset/lazy-entry-eliminated-importers.md new file mode 100644 index 0000000..ed4181f --- /dev/null +++ b/.changeset/lazy-entry-eliminated-importers.md @@ -0,0 +1,5 @@ +--- +'vite-plugin-solid': patch +--- + +Reclassify emitted lazy facade chunks even when their importers are eliminated from the final bundle. Emitted chunk references are now retained so lazy facades can be identified without relying on a surviving `dynamicImports` edge. diff --git a/examples/vite-8/src/App.tsx b/examples/vite-8/src/App.tsx index 84029bf..1454b4a 100644 --- a/examples/vite-8/src/App.tsx +++ b/examples/vite-8/src/App.tsx @@ -1,7 +1,6 @@ import { onSettled } from "solid-js"; import { CounterProvider, useCounter } from "./CounterContext"; - -const title = 'Counter'; +import { title } from './UnusedLazyImporter'; function Count() { const counter = useCounter(); diff --git a/examples/vite-8/src/UnusedLazy.tsx b/examples/vite-8/src/UnusedLazy.tsx new file mode 100644 index 0000000..c87182e --- /dev/null +++ b/examples/vite-8/src/UnusedLazy.tsx @@ -0,0 +1,3 @@ +export default function UnusedLazy() { + return

This lazy component should be removed from the application.

; +} diff --git a/examples/vite-8/src/UnusedLazyImporter.ts b/examples/vite-8/src/UnusedLazyImporter.ts new file mode 100644 index 0000000..7266798 --- /dev/null +++ b/examples/vite-8/src/UnusedLazyImporter.ts @@ -0,0 +1,9 @@ +import { lazy } from 'solid-js'; + +export const title = 'Counter'; + +export class UnusedLazyImporter { + mount() { + return lazy(() => import('./UnusedLazy')); + } +} diff --git a/examples/vite-8/vite.config.ts b/examples/vite-8/vite.config.ts index 6128d7d..6ecabe7 100644 --- a/examples/vite-8/vite.config.ts +++ b/examples/vite-8/vite.config.ts @@ -3,6 +3,33 @@ import solidPlugin from 'vite-plugin-solid'; export default defineConfig({ plugins: [ - solidPlugin({ compiler: 'native' }), + { + name: 'simulate-eliminated-lazy-importer', + enforce: 'pre', + generateBundle(_options, bundle) { + for (const output of Object.values(bundle)) { + if (output.type === 'chunk') { + output.dynamicImports = []; + } + } + }, + }, + solidPlugin({ compiler: 'native', ssr: true }), + { + name: 'assert-single-entry', + enforce: 'post', + generateBundle(_options, bundle) { + const entries = Object.values(bundle).filter( + (output) => output.type === 'chunk' && output.isEntry, + ); + if (entries.length !== 1) { + throw new Error( + `Expected one entry chunk, received: ${entries + .map((entry) => entry.fileName) + .join(', ')}`, + ); + } + }, + }, ], -}); \ No newline at end of file +}); diff --git a/src/index.ts b/src/index.ts index 0aecc77..705df6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -310,6 +310,10 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { // Driven from moduleParsed so it covers every lazy() target, including // import.meta.glob entries that never pass through the moduleUrl transform. const emittedLazyChunks = new Set(); + // Keep the emitted references because a lazy module's importer may be + // removed from the final bundle, leaving no dynamic-import edge to identify + // its facade chunk during generateBundle. + const emittedLazyChunkRefs: string[] = []; // Whether the current hook invocation belongs to a client (browser) build. // Builder-mode builds (e.g. SolidStart's nitro plugin) run the client and @@ -557,7 +561,9 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { if (!/\.[mc]?[tj]sx?$/i.test(cleanId)) continue; if (emittedLazyChunks.has(depId)) continue; emittedLazyChunks.add(depId); - this.emitFile({ type: 'chunk', id: depId, preserveSignature: 'exports-only' }); + emittedLazyChunkRefs.push( + this.emitFile({ type: 'chunk', id: depId, preserveSignature: 'exports-only' }), + ); } }, @@ -623,7 +629,22 @@ export default function solidPlugin(options: Partial = {}): Plugin[] { // serialized manifest read back later) so downstream plugins inspecting // the bundle don't mistake them for application entries. Must precede // the client asset map build, which keys off dynamic entries. - if (options.ssr) normalizeEmittedLazyEntries(bundle); + if (options.ssr) { + for (const ref of emittedLazyChunkRefs) { + let fileName: string; + try { + fileName = this.getFileName(ref); + } catch { + // Ignore references retained from a previous watch build. + continue; + } + const chunk = bundle[fileName]; + if (!chunk || chunk.type !== 'chunk') continue; + chunk.isEntry = false; + chunk.isDynamicEntry = true; + } + normalizeEmittedLazyEntries(bundle); + } substituteClientManifest(bundle, buildClientAssetMap(bundle, projectRoot, base)); },