|
| 1 | +# vite-svelte-dedupe |
| 2 | + |
| 3 | +A messy remnant of my pre-bundling journey. |
| 4 | + |
| 5 | +> Below is my notes taken |
| 6 | +
|
| 7 | +# Problem |
| 8 | + |
| 9 | +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](https://github.com/sveltejs/svelte/blob/c5f588ee50a50a77bb22ba006ee04f66795de74f/src/runtime/internal/lifecycle.ts#L3). Among other things. |
| 10 | + |
| 11 | +# Goal |
| 12 | + |
| 13 | +Dedupe Svelte dependency to ensure runtime is shared in pre-bundling and in user code. |
| 14 | + |
| 15 | +# Travels |
| 16 | + |
| 17 | +The pre-bundling process uses esbuild. Esbuild is ran twice by Vite, one for import scans, one for the actula pre-bundling. The focus is on the latter. [Here's the relevant code](https://github.com/vitejs/vite/blob/9abdb8137ef54dd095e7bc47ae6a1ccf490fd196/packages/vite/src/node/optimizer/index.ts#L262). |
| 18 | + |
| 19 | +## external |
| 20 | + |
| 21 | +At first glance, you would notice [`external: config.optimizeDeps?.exclude,`](https://github.com/vitejs/vite/blob/9abdb8137ef54dd095e7bc47ae6a1ccf490fd196/packages/vite/src/node/optimizer/index.ts#L266). One would assume that since `vite-plugin-svelte` excludes [Svelte's import paths](https://github.com/sveltejs/vite-plugin-svelte/blob/09b63d32e8816acc554a66d4d01062be197dfbb7/packages/vite-plugin-svelte/src/index.ts#L61) (`svelte`, `svelte/store`, etc), ideally the generated pre-bundled file would not bundle the Svelte library. |
| 22 | + |
| 23 | +Turns out this is not true, not because of esbuild, but because of Vite's [`esbuildDepsPlugin`](https://github.com/vitejs/vite/blob/9abdb8137ef54dd095e7bc47ae6a1ccf490fd196/packages/vite/src/node/optimizer/esbuildDepPlugin.ts). The issue is that Vite applies a [custom resolver algorithm](https://github.com/vitejs/vite/blob/9abdb8137ef54dd095e7bc47ae6a1ccf490fd196/packages/vite/src/node/optimizer/esbuildDepPlugin.ts#L103-L138) 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). |
| 24 | + |
| 25 | +## patch external |
| 26 | + |
| 27 | +We could apply a patch to the issue above by adding this code below [this line](https://github.com/vitejs/vite/blob/9abdb8137ef54dd095e7bc47ae6a1ccf490fd196/packages/vite/src/node/optimizer/esbuildDepPlugin.ts#L134): |
| 28 | + |
| 29 | +```js |
| 30 | +external: build.initialOptions.external?.includes(id) |
| 31 | +``` |
| 32 | +
|
| 33 | +Now, external will be respected. |
| 34 | +
|
| 35 | +> You might notice the bundled code has [repeated imports](https://github.com/evanw/esbuild/issues/475) by esbuild, though this is harmless in our scenario). |
| 36 | +
|
| 37 | +However, this patch doesn't work in the big picture, because: |
| 38 | +
|
| 39 | +1. In user code, Vite transforms the Svelte import path to, e.g. `/node_modules/.pnpm/svelte@3.38.2/node_modules/svelte/index.mjs?v=abc123` |
| 40 | +2. In prebundled code, the import path is, e.g. `/node_modules/.pnpm/svelte@3.38.2/node_modules/svelte/index.mjs` |
| 41 | +
|
| 42 | +The query string returns two different Svelte instance for each requested script. This may explain why Vite intentionally affect the external algorithm. |
| 43 | +
|
| 44 | +**This route doesn't work.** |
| 45 | +
|
| 46 | +## optimize svelte |
| 47 | +
|
| 48 | +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. |
| 49 | +
|
| 50 | +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). |
| 51 | +
|
| 52 | +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. |
| 53 | +
|
| 54 | +**There is another problem.** |
| 55 | +
|
| 56 | +## library not dedupe |
| 57 | +
|
| 58 | +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. |
| 59 | +
|
| 60 | +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. |
| 61 | +
|
| 62 | +When Vite optimizes this (entrypoint `cmp/index.js`), Vite will [ignore Svelte file extensions](https://github.com/vitejs/vite/blob/9aa255a0abcb9f5b23c34607b2188f796f4b6c94/packages/vite/src/node/optimizer/esbuildDepPlugin.ts#L71-L85) (among other types) in the bundle, make sense as we shouldn't bundle Svelte components. |
| 63 | +
|
| 64 | +But there's a problem, taking a look at `cmp/Route.svelte`, you'll notice that it imports a path to `./../dist/tinro_lib`, and in the network request, you can see that the path is transformed directly to the actual file in node_modules (not the pre-bundled file in `.vite`). |
| 65 | +
|
| 66 | +And this **fails dedupe for the library itself**, not Svelte anymore. This is likely the root cause why some Svelte libraries work oddly. |
| 67 | +
|
| 68 | +# Solution |
| 69 | +
|
| 70 | +Based on the root cause, this happens to any extensions [listed here](https://github.com/vitejs/vite/blob/9aa255a0abcb9f5b23c34607b2188f796f4b6c94/packages/vite/src/node/optimizer/esbuildDepPlugin.ts#L15-L32). I have not tested this on other extensions. |
| 71 | +
|
| 72 | +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. |
| 73 | +
|
| 74 | +If this happens to be a Svelte specific thing, we could auto externalize all Svelte libraries based on `pkg.svelte`. |
0 commit comments