Skip to content

feat: prebundle svelte libraries #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-pets-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': minor
---

Add `experimental.prebundleSvelteLibraries` option
13 changes: 13 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,19 @@ export default defineConfig({

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

### prebundleSvelteLibraries

- **Type:** `boolean`
- **Default:** `false`

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.

Setting this option to `true` will directly pre-bundle Svelte libraries, which should improve initial page load performance. However, please note some caveats:

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.

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.

### generateMissingPreprocessorSourcemaps

- **Type:** `boolean`
Expand Down
4 changes: 3 additions & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ For reference, check out [windicss](https://github.com/windicss/vite-plugin-wind

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.

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.
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.

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)

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).
3 changes: 3 additions & 0 deletions packages/playground/big-component-library/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# do not use as a starter

This example is an app using a large component library
12 changes: 12 additions & 0 deletions packages/playground/big-component-library/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Svelte + Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
34 changes: 34 additions & 0 deletions packages/playground/big-component-library/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "esnext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}
17 changes: 17 additions & 0 deletions packages/playground/big-component-library/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "playground-big-component-library",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "workspace:*",
"carbon-components-svelte": "^0.45.1",
"svelte": "^3.43.2",
"vite": "^2.6.7"
}
}
11 changes: 11 additions & 0 deletions packages/playground/big-component-library/src/App.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
import { Accordion, AccordionItem, Button } from 'carbon-components-svelte';
</script>

<Button>Hello</Button>

<Accordion>
<AccordionItem title="Section 1" open>Content 1</AccordionItem>
<AccordionItem title="Section 2">Content 2</AccordionItem>
<AccordionItem title="Section 3">Content 3</AccordionItem>
</Accordion>
8 changes: 8 additions & 0 deletions packages/playground/big-component-library/src/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import App from './App.svelte';
import 'carbon-components-svelte/css/white.css';

const app = new App({
target: document.getElementById('app')
});

export default app;
12 changes: 12 additions & 0 deletions packages/playground/big-component-library/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [
svelte({
experimental: {
prebundleSvelteLibraries: true
}
})
]
});
94 changes: 94 additions & 0 deletions packages/vite-plugin-svelte/src/utils/esbuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { promises as fs } from 'fs';
import { compile, preprocess } from 'svelte/compiler';
import { DepOptimizationOptions } from 'vite';
import { Compiled } from './compile';
import { log } from './log';
import { CompileOptions, ResolvedOptions } from './options';

type EsbuildOptions = NonNullable<DepOptimizationOptions['esbuildOptions']>;
type EsbuildPlugin = NonNullable<EsbuildOptions['plugins']>[number];
type EsbuildPluginBuild = Parameters<EsbuildPlugin['setup']>[0];

export function esbuildSveltePlugin(options: ResolvedOptions): EsbuildPlugin {
return {
name: 'vite-plugin-svelte:optimize-svelte',
setup(build) {
disableVitePrebundleSvelte(build);

const svelteExtensions = (options.extensions ?? ['.svelte']).map((ext) => ext.slice(1));
const svelteFilter = new RegExp(`\\.(` + svelteExtensions.join('|') + `)(\\?.*)?$`);

build.onLoad({ filter: svelteFilter }, async ({ path: filename }) => {
const code = await fs.readFile(filename, 'utf8');
const contents = await compileSvelte(options, { filename, code });
return { contents };
});
}
};
}

function disableVitePrebundleSvelte(build: EsbuildPluginBuild) {
const viteDepPrebundlePlugin = build.initialOptions.plugins?.find(
(v) => v.name === 'vite:dep-pre-bundle'
);

if (!viteDepPrebundlePlugin) return;

// Prevent vite:dep-pre-bundle from externalizing svelte files
const _setup = viteDepPrebundlePlugin.setup.bind(viteDepPrebundlePlugin);
viteDepPrebundlePlugin.setup = function (build) {
const _onResolve = build.onResolve.bind(build);
build.onResolve = function (options, callback) {
if (options.filter.source.includes('svelte')) {
options.filter = new RegExp(
options.filter.source.replace('|svelte', ''),
options.filter.flags
);
}
return _onResolve(options, callback);
};
return _setup(build);
};
}

async function compileSvelte(
options: ResolvedOptions,
{ filename, code }: { filename: string; code: string }
): Promise<string> {
const compileOptions: CompileOptions = {
...options.compilerOptions,
css: true,
filename,
generate: 'dom'
};

let preprocessed;

if (options.preprocess) {
preprocessed = await preprocess(code, options.preprocess, { filename });
if (preprocessed.map) compileOptions.sourcemap = preprocessed.map;
}

const finalCode = preprocessed ? preprocessed.code : code;

const dynamicCompileOptions = await options.experimental?.dynamicCompileOptions?.({
filename,
code: finalCode,
compileOptions
});

if (dynamicCompileOptions && log.debug.enabled) {
log.debug(`dynamic compile options for ${filename}: ${JSON.stringify(dynamicCompileOptions)}`);
}

const finalCompileOptions = dynamicCompileOptions
? {
...compileOptions,
...dynamicCompileOptions
}
: compileOptions;

const compiled = compile(finalCode, finalCompileOptions) as Compiled;

return compiled.js.code + '//# sourceMappingURL=' + compiled.js.map.toUrl();
}
24 changes: 22 additions & 2 deletions packages/vite-plugin-svelte/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import path from 'path';
import { findRootSvelteDependencies, needsOptimization, SvelteDependency } from './dependencies';
import { createRequire } from 'module';
import { esbuildSveltePlugin } from './esbuild';

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

// If we prebundle svelte libraries, we can skip the whole prebundling dance below
if (options.experimental.prebundleSvelteLibraries) {
return {
include,
exclude,
esbuildOptions: {
plugins: [esbuildSveltePlugin(options)]
}
};
}

// only svelte component libraries needs to be processed for optimizeDeps, js libraries work fine
svelteDeps = svelteDeps.filter((dep) => dep.type === 'component-library');

const svelteDepsToExclude = Array.from(new Set(svelteDeps.map((dep) => dep.name))).filter(
(dep) => !isIncluded(dep)
);
Expand Down Expand Up @@ -420,6 +433,13 @@ export interface ExperimentalOptions {
*/
useVitePreprocess?: boolean;

/**
* Force Vite to pre-bundle Svelte libraries
*
* @default false
*/
prebundleSvelteLibraries?: boolean;

/**
* If a preprocessor does not provide a sourcemap, a best-effort fallback sourcemap will be provided.
* This option requires `diff-match-patch` to be installed as a peer dependency.
Expand Down
27 changes: 27 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.