Skip to content

Commit 611b0d8

Browse files
committed
pain
0 parents  commit 611b0d8

11 files changed

+445
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
node_modules

README.md

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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`.

index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Svelte + Vite App</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.js"></script>
12+
</body>
13+
</html>

jsconfig.json

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "node",
4+
"target": "esnext",
5+
"module": "esnext",
6+
/**
7+
* svelte-preprocess cannot figure out whether you have
8+
* a value or a type, so tell TypeScript to enforce using
9+
* `import type` instead of `import` for Types.
10+
*/
11+
"importsNotUsedAsValues": "error",
12+
"isolatedModules": true,
13+
"resolveJsonModule": true,
14+
/**
15+
* To have warnings / errors of the Svelte compiler at the
16+
* correct position, enable source maps by default.
17+
*/
18+
"sourceMap": true,
19+
"esModuleInterop": true,
20+
"skipLibCheck": true,
21+
"forceConsistentCasingInFileNames": true,
22+
"baseUrl": ".",
23+
/**
24+
* Typecheck JS in `.svelte` and `.js` files by default.
25+
* Disable this if you'd like to use dynamic types.
26+
*/
27+
"checkJs": true
28+
},
29+
/**
30+
* Use global.d.ts instead of compilerOptions.types
31+
* to avoid limiting type declarations.
32+
*/
33+
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
34+
}

package.json

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "vite-svelte-dedupe",
3+
"private": true,
4+
"scripts": {
5+
"dev": "vite",
6+
"build": "vite build",
7+
"serve": "vite preview"
8+
},
9+
"devDependencies": {
10+
"@sveltejs/vite-plugin-svelte": "link:../vite-plugin-svelte/packages/vite-plugin-svelte",
11+
"svelte": "^3.38.2",
12+
"svelte-select": "^3.17.0",
13+
"tinro": "^0.6.4",
14+
"vite": "link:../vite/packages/vite"
15+
}
16+
}

pnpm-lock.yaml

+219
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/favicon.ico

1.12 KB
Binary file not shown.

src/App.svelte

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<script>
2+
import { Route, router } from 'tinro'
3+
// import Select from 'svelte-select'
4+
5+
// let items = [
6+
// { value: 'chocolate', label: 'Chocolate' },
7+
// { value: 'pizza', label: 'Pizza' },
8+
// { value: 'cake', label: 'Cake' },
9+
// { value: 'chips', label: 'Chips' },
10+
// { value: 'ice-cream', label: 'Ice Cream' },
11+
// ]
12+
13+
// let selectedValue = { value: 'cake', label: 'Cake' }
14+
15+
// function handleSelect(event) {
16+
// console.log('selected item', event.detail)
17+
// // .. do something here 🙂
18+
// }
19+
</script>
20+
21+
{JSON.stringify($router)}
22+
<nav>
23+
<a href="/foo">To foo</a>
24+
<a href="/bar">To bar</a>
25+
<button on:click={() => router.goto('/bar')}>Go bar</button>
26+
</nav>
27+
28+
<!-- <Select {items} {selectedValue} on:select={handleSelect} /> -->
29+
30+
<Route path="/foo">Foo</Route>
31+
<Route path="/bar">Bar</Route>
32+
<Route fallback>Fallback</Route>

0 commit comments

Comments
 (0)