Skip to content

Commit 6f3fabc

Browse files
authored
feat: prebundle svelte libraries (#200)
* feat: prebundle svelte libraries * docs: add prebundleSvelteLibraries * chore: add changeset
1 parent 1c36fc0 commit 6f3fabc

File tree

13 files changed

+261
-3
lines changed

13 files changed

+261
-3
lines changed

.changeset/loud-pets-cheat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/vite-plugin-svelte': minor
3+
---
4+
5+
Add `experimental.prebundleSvelteLibraries` option

docs/config.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,19 @@ export default defineConfig({
193193

194194
Use extra preprocessors that delegate style and TypeScript preprocessing to native Vite plugins. Do not use together with `svelte-preprocess`!
195195

196+
### prebundleSvelteLibraries
197+
198+
- **Type:** `boolean`
199+
- **Default:** `false`
200+
201+
Force Vite to pre-bundle Svelte libraries. Currently, `vite-plugin-svelte` implements a complex mechanism to address pre-bundling Svelte libraries, which had an impact on large Svelte component libraries and the initial page load. See the [FAQ](./faq.md#what-is-going-on-with-vite-and-pre-bundling-dependencies) for more information.
202+
203+
Setting this option to `true` will directly pre-bundle Svelte libraries, which should improve initial page load performance. However, please note some caveats:
204+
205+
1. Deeply importing Svelte components is not supported. Either import all components from one entrypoint, or always stick to deep imports, otherwise it could cause multiple instance of the Svelte library running.
206+
207+
2. When updating the Svelte compiler options in `svelte.config.js` or `vite.config.js`, delete the `node_modules/.vite` folder to trigger pre-bundling in Vite again.
208+
196209
### generateMissingPreprocessorSourcemaps
197210

198211
- **Type:** `boolean`

docs/faq.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ For reference, check out [windicss](https://github.com/windicss/vite-plugin-wind
9595

9696
Pre-bundling dependencies is an [optimization in Vite](https://vitejs.dev/guide/dep-pre-bundling.html). It is required for CJS dependencies, as Vite's development server only works with ES modules on the client side.
9797

98-
Thanks to [a new API in Vite](https://github.com/vitejs/vite/pull/4634), [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte/pull/157) automatically handles pre-bundling these for you.
98+
Historically, Svelte components had issues being pre-bundled, causing [deduplication issues](https://github.com/vitejs/vite/issues/3910) and [CJS interoperability issues](https://github.com/vitejs/vite/issues/3024). Since Vite 2.5.2, [a new API in Vite](https://github.com/vitejs/vite/pull/4634) allowed us to [automatically handle Svelte library pre-bundling](https://github.com/sveltejs/vite-plugin-svelte/pull/157) for you.
99+
100+
Today, this feature had served us well, however a caveat remained that large Svelte component libraries often slows down the initial page load. If this affects you, try setting [experimental.prebundleSvelteLibraries](./config.md#prebundleSvelteLibraries) option to `true` to speed things up. (Note that it's an experimental option that may not always work)
99101

100102
In case you still run into errors like `The requested module 'xxx' does not provide an export named 'yyy'`, please check our [open issues](https://github.com/sveltejs/vite-plugin-svelte/issues).
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# do not use as a starter
2+
3+
This example is an app using a large component library
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Svelte + Vite App</title>
7+
</head>
8+
<body>
9+
<div id="app"></div>
10+
<script type="module" src="/src/main.js"></script>
11+
</body>
12+
</html>
Lines changed: 34 additions & 0 deletions
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+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "playground-big-component-library",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "vite build",
9+
"serve": "vite preview"
10+
},
11+
"devDependencies": {
12+
"@sveltejs/vite-plugin-svelte": "workspace:*",
13+
"carbon-components-svelte": "^0.45.1",
14+
"svelte": "^3.43.2",
15+
"vite": "^2.6.7"
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<script>
2+
import { Accordion, AccordionItem, Button } from 'carbon-components-svelte';
3+
</script>
4+
5+
<Button>Hello</Button>
6+
7+
<Accordion>
8+
<AccordionItem title="Section 1" open>Content 1</AccordionItem>
9+
<AccordionItem title="Section 2">Content 2</AccordionItem>
10+
<AccordionItem title="Section 3">Content 3</AccordionItem>
11+
</Accordion>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import App from './App.svelte';
2+
import 'carbon-components-svelte/css/white.css';
3+
4+
const app = new App({
5+
target: document.getElementById('app')
6+
});
7+
8+
export default app;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { svelte } from '@sveltejs/vite-plugin-svelte';
2+
import { defineConfig } from 'vite';
3+
4+
export default defineConfig({
5+
plugins: [
6+
svelte({
7+
experimental: {
8+
prebundleSvelteLibraries: true
9+
}
10+
})
11+
]
12+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { promises as fs } from 'fs';
2+
import { compile, preprocess } from 'svelte/compiler';
3+
import { DepOptimizationOptions } from 'vite';
4+
import { Compiled } from './compile';
5+
import { log } from './log';
6+
import { CompileOptions, ResolvedOptions } from './options';
7+
8+
type EsbuildOptions = NonNullable<DepOptimizationOptions['esbuildOptions']>;
9+
type EsbuildPlugin = NonNullable<EsbuildOptions['plugins']>[number];
10+
type EsbuildPluginBuild = Parameters<EsbuildPlugin['setup']>[0];
11+
12+
export function esbuildSveltePlugin(options: ResolvedOptions): EsbuildPlugin {
13+
return {
14+
name: 'vite-plugin-svelte:optimize-svelte',
15+
setup(build) {
16+
disableVitePrebundleSvelte(build);
17+
18+
const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
19+
const svelteFilter = new RegExp(`\\.(` + svelteExtensions.join('|') + `)(\\?.*)?$`);
20+
21+
build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
22+
const code = await fs.readFile(filename, 'utf8');
23+
const contents = await compileSvelte(options, { filename, code });
24+
return { contents };
25+
});
26+
}
27+
};
28+
}
29+
30+
function disableVitePrebundleSvelte(build: EsbuildPluginBuild) {
31+
const viteDepPrebundlePlugin = build.initialOptions.plugins?.find(
32+
(v) => v.name === 'vite:dep-pre-bundle'
33+
);
34+
35+
if (!viteDepPrebundlePlugin) return;
36+
37+
// Prevent vite:dep-pre-bundle from externalizing svelte files
38+
const _setup = viteDepPrebundlePlugin.setup.bind(viteDepPrebundlePlugin);
39+
viteDepPrebundlePlugin.setup = function (build) {
40+
const _onResolve = build.onResolve.bind(build);
41+
build.onResolve = function (options, callback) {
42+
if (options.filter.source.includes('svelte')) {
43+
options.filter = new RegExp(
44+
options.filter.source.replace('|svelte', ''),
45+
options.filter.flags
46+
);
47+
}
48+
return _onResolve(options, callback);
49+
};
50+
return _setup(build);
51+
};
52+
}
53+
54+
async function compileSvelte(
55+
options: ResolvedOptions,
56+
{ filename, code }: { filename: string; code: string }
57+
): Promise<string> {
58+
const compileOptions: CompileOptions = {
59+
...options.compilerOptions,
60+
css: true,
61+
filename,
62+
generate: 'dom'
63+
};
64+
65+
let preprocessed;
66+
67+
if (options.preprocess) {
68+
preprocessed = await preprocess(code, options.preprocess, { filename });
69+
if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
70+
}
71+
72+
const finalCode = preprocessed ? preprocessed.code : code;
73+
74+
const dynamicCompileOptions = await options.experimental?.dynamicCompileOptions?.({
75+
filename,
76+
code: finalCode,
77+
compileOptions
78+
});
79+
80+
if (dynamicCompileOptions && log.debug.enabled) {
81+
log.debug(`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`);
82+
}
83+
84+
const finalCompileOptions = dynamicCompileOptions
85+
? {
86+
...compileOptions,
87+
...dynamicCompileOptions
88+
}
89+
: compileOptions;
90+
91+
const compiled = compile(finalCode, finalCompileOptions) as Compiled;
92+
93+
return compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl();
94+
}

packages/vite-plugin-svelte/src/utils/options.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import path from 'path';
1616
import { findRootSvelteDependencies, needsOptimization, SvelteDependency } from './dependencies';
1717
import { createRequire } from 'module';
18+
import { esbuildSveltePlugin } from './esbuild';
1819

1920
const knownOptions = new Set([
2021
'configFile',
@@ -217,8 +218,6 @@ function buildOptimizeDepsForSvelte(
217218
options: ResolvedOptions,
218219
optimizeDeps?: DepOptimizationOptions
219220
): DepOptimizationOptions {
220-
// only svelte component libraries needs to be processed for optimizeDeps, js libraries work fine
221-
svelteDeps = svelteDeps.filter((dep) => dep.type === 'component-library');
222221
// include svelte imports for optimization unless explicitly excluded
223222
const include: string[] = [];
224223
const exclude: string[] = ['svelte-hmr'];
@@ -241,6 +240,20 @@ function buildOptimizeDepsForSvelte(
241240
log.debug('"svelte" is excluded in optimizeDeps.exclude, skipped adding it to include.');
242241
}
243242

243+
// If we prebundle svelte libraries, we can skip the whole prebundling dance below
244+
if (options.experimental.prebundleSvelteLibraries) {
245+
return {
246+
include,
247+
exclude,
248+
esbuildOptions: {
249+
plugins: [esbuildSveltePlugin(options)]
250+
}
251+
};
252+
}
253+
254+
// only svelte component libraries needs to be processed for optimizeDeps, js libraries work fine
255+
svelteDeps = svelteDeps.filter((dep) => dep.type === 'component-library');
256+
244257
const svelteDepsToExclude = Array.from(new Set(svelteDeps.map((dep) => dep.name))).filter(
245258
(dep) => !isIncluded(dep)
246259
);
@@ -420,6 +433,13 @@ export interface ExperimentalOptions {
420433
*/
421434
useVitePreprocess?: boolean;
422435

436+
/**
437+
* Force Vite to pre-bundle Svelte libraries
438+
*
439+
* @default false
440+
*/
441+
prebundleSvelteLibraries?: boolean;
442+
423443
/**
424444
* If a preprocessor does not provide a sourcemap, a best-effort fallback sourcemap will be provided.
425445
* This option requires `diff-match-patch` to be installed as a peer dependency.

pnpm-lock.yaml

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)