Pre-bundled dependencies doesn't dedupe imports in external files. Specifically in this repo, Svelte files are the external files.
pnpm i
.pnpm dev
.- Go to http://localhost:3000.
- Click on "To bar" link, text change to "Bar".
- Refresh http://localhost:3000.
- Click on "Go bar" button, text doesn't change (It should change to "Bar").
The problem is that the tinro
dependency's code isn't deduped due to the use of Svelte files. Below explains the issue in detail, the main paragraph is library not dedupe.
This is also not an issue within tinro
as it works properly if we add it to optimizeDeps.exclude
. tinro
is used as the dependency to be pre-bundled.
NOTE: The part below isn't relevant to the repro, but it documents some issues/learnings I find while fixing Vite + Svelte pre-bundling issue.
In Vite + Svelte integration, some Svelte libraries needs to be excluded from optimization as Vite's pre-bundling process would bundle the Svelte's runtime all together. Svelte's runtime is a singleton so some functions like setContext
would fail since it accesses to global current_component
variable. Among other things.
Dedupe Svelte dependency to ensure runtime is shared in pre-bundling and in user code.
The pre-bundling process uses esbuild. Esbuild is ran twice by Vite, one for import scans, one for the actual pre-bundling. The focus is on the latter. Here's the relevant code.
At first glance, you would notice external: config.optimizeDeps?.exclude,
. One would assume that since vite-plugin-svelte
excludes Svelte's import paths (svelte
, svelte/store
, etc), ideally the generated pre-bundled file would not bundle the Svelte library.
Turns out this is not true, not because of esbuild, but because of Vite's esbuildDepsPlugin
. The issue is that Vite applies a custom resolver algorithm over esbuild's, which indirectly affects what dependency gets externalized. In other words, only dependencies which can't be resolved are externalized (Not sure if there's any use for this behaviour).
We could apply a patch to the issue above by adding this code below this line:
external: build.initialOptions.external?.includes(id)
Now, external will be respected.
You might notice the bundled code has repeated imports by esbuild, though this is harmless in our scenario).
However, this patch doesn't work in the big picture, because:
- In user code, Vite transforms the Svelte import path to, e.g.
/node_modules/.pnpm/[email protected]/node_modules/svelte/index.mjs?v=abc123
- In pre-bundled code, the import path is, e.g.
/node_modules/.pnpm/[email protected]/node_modules/svelte/index.mjs
The query string returns two different Svelte instance for each requested script. This may explain why Vite intentionally affect the external algorithm.
This route doesn't work.
Taking a step back, what if we include Svelte libraries into the pre-bundling process, that way bundled code and user code can reference the same Svelte instance.
A change in vite-plugin-svelte is needed by adding svelte
and svelte/*
imports in optimizeDeps.include
, the Svelte instance is successfully deduped. Checking the network request, I can confirm the Svelte library is only requested once (deduped).
But there still exist an odd behaviour. The victim I used, tinro
, still isn't working properly when calling router.goto
, the route won't get updated.
There is another problem.
After countless hours of debugging, finally my eye was caught on an oddity in the "Sources" tab. First you would have to understand tinro's build directory, https://www.jsdelivr.com/package/npm/tinro.
The notable files are cmp/Route.svelte
, cmp/index.js
, and dist/tinro_lib.js
. Take a look at the contents of the first two files.
When Vite optimizes this (entrypoint cmp/index.js
), Vite will ignore Svelte file extensions (among other types) in the bundle, make sense as we shouldn't bundle Svelte components.
But there's a problem, taking a look at cmp/Route.svelte
, you'll notice that it imports a path to ./../dist/tinro_lib
. Going back to the running Vite app, the network request shows that it's transformed to the following import:
// http://localhost:3000/node_modules/.pnpm/[email protected]/node_modules/tinro/cmp/Route.svelte
// ...
import { createRouteObject } from '/node_modules/.pnpm/[email protected]/node_modules/tinro/dist/tinro_lib.js'
//...
tinro
's code is now duplicated between tinro_lib.js
(from node_modules/.pnpm
) and tinro.js
(from node_modules/.vite
), and this fails dedupe for the library itself, not Svelte anymore. This is likely the root cause why some Svelte libraries work oddly.
Based on the root cause, this happens to any extensions listed here. I have not tested this on other extensions like .vue
or .tsx
as these files are usually compiled in library builds.
I don't have a solid strategy to tackle this, but one naive implementation would be to scan these subpaths as entrypoints for the pre-bundling, so esbuild would generate the chunks for these files to use.