From 913f8d88affe7f5be3539f066d34ad45fbceae18 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 30 Mar 2026 20:31:23 +0800 Subject: [PATCH 1/3] docs: add lazy loading plugins documentation and tests Document the `lazy` field in config page with usage examples showing how to defer heavy plugin imports using async/await dynamic imports. Add unit tests for async/await lazy loading patterns and a snap test that verifies lazy-loaded plugins are applied during vp build. --- docs/config/index.md | 63 +++++++++++++++++++ .../lazy-loading-plugins/index.html | 8 +++ .../lazy-loading-plugins/my-plugin.ts | 8 +++ .../lazy-loading-plugins/package.json | 4 ++ .../snap-tests/lazy-loading-plugins/snap.txt | 4 ++ .../lazy-loading-plugins/steps.json | 10 +++ .../lazy-loading-plugins/vite.config.ts | 8 +++ packages/cli/src/__tests__/index.spec.ts | 60 ++++++++++++++++++ 8 files changed, 165 insertions(+) create mode 100644 packages/cli/snap-tests/lazy-loading-plugins/index.html create mode 100644 packages/cli/snap-tests/lazy-loading-plugins/my-plugin.ts create mode 100644 packages/cli/snap-tests/lazy-loading-plugins/package.json create mode 100644 packages/cli/snap-tests/lazy-loading-plugins/snap.txt create mode 100644 packages/cli/snap-tests/lazy-loading-plugins/steps.json create mode 100644 packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts diff --git a/docs/config/index.md b/docs/config/index.md index 78807ce104..8fc06cda22 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -29,3 +29,66 @@ Vite+ extends the basic Vite configuration with these additions: - [`run`](/config/run) for Vite Task - [`pack`](/config/pack) for tsdown - [`staged`](/config/staged) for staged-file checks + +## Lazy Loading Plugins + +When `vite.config.ts` imports heavy plugins at the top level, every `import` is evaluated eagerly, even for commands like `vp lint` or `vp fmt` that don't need those plugins. This can make config loading noticeably slow. + +The `lazy` field solves this by letting you defer plugin loading into an async function. Plugins provided through `lazy` are only resolved when actually needed: + +```ts +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lazy: async () => { + const { default: myHeavyPlugins } = await import('./my-heavy-plugins'); + return { plugins: myHeavyPlugins }; + }, +}); +``` + +### Type Signature + +```ts +lazy?: () => Promise<{ + plugins?: Plugin[]; +}>; +``` + +### Merging with Existing Plugins + +Plugins returned from `lazy` are appended after any plugins already in the `plugins` array. This lets you keep lightweight plugins inline and defer only the expensive ones: + +```ts +import { defineConfig } from 'vite-plus'; +import lightPlugin from 'vite-plugin-light'; + +export default defineConfig({ + plugins: [lightPlugin()], + lazy: async () => { + const { default: heavyPlugin } = await import('vite-plugin-heavy'); + return { plugins: [heavyPlugin()] }; + }, +}); +``` + +The resulting plugin order is: `[lightPlugin(), heavyPlugin()]`. + +### Function Config + +`lazy` also works with function-style and async function-style configs: + +```ts +import { defineConfig } from 'vite-plus'; + +export default defineConfig(async () => ({ + lazy: async () => { + const { default: heavyPlugin } = await import('vite-plugin-heavy'); + return { plugins: [heavyPlugin()] }; + }, +})); +``` + +::: info +The `lazy` field is a temporary Vite+ extension. We plan to support this in upstream Vite in the future. +::: diff --git a/packages/cli/snap-tests/lazy-loading-plugins/index.html b/packages/cli/snap-tests/lazy-loading-plugins/index.html new file mode 100644 index 0000000000..7febab8c7e --- /dev/null +++ b/packages/cli/snap-tests/lazy-loading-plugins/index.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/cli/snap-tests/lazy-loading-plugins/my-plugin.ts b/packages/cli/snap-tests/lazy-loading-plugins/my-plugin.ts new file mode 100644 index 0000000000..33ba10fcfe --- /dev/null +++ b/packages/cli/snap-tests/lazy-loading-plugins/my-plugin.ts @@ -0,0 +1,8 @@ +export default function myLazyPlugin() { + return { + name: 'my-lazy-plugin', + transformIndexHtml(html: string) { + return html.replace('', ''); + }, + }; +} diff --git a/packages/cli/snap-tests/lazy-loading-plugins/package.json b/packages/cli/snap-tests/lazy-loading-plugins/package.json new file mode 100644 index 0000000000..be4b007f68 --- /dev/null +++ b/packages/cli/snap-tests/lazy-loading-plugins/package.json @@ -0,0 +1,4 @@ +{ + "name": "lazy-loading-plugins-test", + "private": true +} diff --git a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt b/packages/cli/snap-tests/lazy-loading-plugins/snap.txt new file mode 100644 index 0000000000..5ad97fc496 --- /dev/null +++ b/packages/cli/snap-tests/lazy-loading-plugins/snap.txt @@ -0,0 +1,4 @@ +> # Test that plugins loaded via lazy field are applied during build +> vp build +> cat dist/index.html | grep 'lazy-plugin-injected' + diff --git a/packages/cli/snap-tests/lazy-loading-plugins/steps.json b/packages/cli/snap-tests/lazy-loading-plugins/steps.json new file mode 100644 index 0000000000..8139fcae5f --- /dev/null +++ b/packages/cli/snap-tests/lazy-loading-plugins/steps.json @@ -0,0 +1,10 @@ +{ + "commands": [ + "# Test that plugins loaded via lazy field are applied during build", + { + "command": "vp build", + "ignoreOutput": true + }, + "cat dist/index.html | grep 'lazy-plugin-injected'" + ] +} diff --git a/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts b/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts new file mode 100644 index 0000000000..3875756cf2 --- /dev/null +++ b/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + lazy: async () => { + const { default: myLazyPlugin } = await import('./my-plugin'); + return { plugins: [myLazyPlugin()] }; + }, +}); diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts index 072f85683b..f8f641105e 100644 --- a/packages/cli/src/__tests__/index.spec.ts +++ b/packages/cli/src/__tests__/index.spec.ts @@ -141,3 +141,63 @@ test('should handle async function config without lazy', async () => { expect(config.plugins?.length).toBe(1); expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy'); }); + +test('should support async/await lazy loading of plugins', async () => { + const config = await defineConfig({ + lazy: async () => { + const plugins = [{ name: 'async-lazy' }]; + return { plugins }; + }, + }); + expect(config.plugins?.length).toBe(1); + expect((config.plugins?.[0] as { name: string })?.name).toBe('async-lazy'); +}); + +test('should merge async/await lazy plugins with existing plugins', async () => { + const config = await defineConfig({ + plugins: [{ name: 'existing' }], + lazy: async () => { + const plugins = [{ name: 'async-lazy' }]; + return { plugins }; + }, + }); + expect(config.plugins?.length).toBe(2); + expect((config.plugins?.[0] as { name: string })?.name).toBe('existing'); + expect((config.plugins?.[1] as { name: string })?.name).toBe('async-lazy'); +}); + +test('should support async/await lazy with dynamic import pattern', async () => { + const config = await defineConfig({ + lazy: async () => { + // simulates: const { default: plugin } = await import('heavy-plugin') + const plugin = await Promise.resolve({ name: 'dynamic-import-plugin' }); + return { plugins: [plugin] }; + }, + }); + expect(config.plugins?.length).toBe(1); + expect((config.plugins?.[0] as { name: string })?.name).toBe('dynamic-import-plugin'); +}); + +test('should support async/await lazy in async function config', async () => { + const configFn = defineConfig(async () => ({ + lazy: async () => { + const plugins = [{ name: 'async-fn-async-lazy' }]; + return { plugins }; + }, + })); + const config = await configFn({ command: 'build', mode: 'production' }); + expect(config.plugins?.length).toBe(1); + expect((config.plugins?.[0] as { name: string })?.name).toBe('async-fn-async-lazy'); +}); + +test('should support async/await lazy in sync function config', async () => { + const configFn = defineConfig(() => ({ + lazy: async () => { + const plugins = [{ name: 'sync-fn-async-lazy' }]; + return { plugins }; + }, + })); + const config = await configFn({ command: 'build', mode: 'production' }); + expect(config.plugins?.length).toBe(1); + expect((config.plugins?.[0] as { name: string })?.name).toBe('sync-fn-async-lazy'); +}); From 8d0889e764959752472110396d4b7f554f01bc16 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 31 Mar 2026 11:07:47 +0800 Subject: [PATCH 2/3] docs: move lazy loading docs to config troubleshooting page Use Vite's native Promise-based plugin support via dynamic import() instead of the custom lazy field. Migrate the documentation from config/index.md to a new config/troubleshooting.md page and update the snap test to use the import().then() pattern. --- docs/.vitepress/config.mts | 1 + docs/config/index.md | 65 +------------------ docs/config/troubleshooting.md | 35 ++++++++++ .../snap-tests/lazy-loading-plugins/snap.txt | 2 +- .../lazy-loading-plugins/steps.json | 2 +- .../lazy-loading-plugins/vite.config.ts | 5 +- 6 files changed, 40 insertions(+), 70 deletions(-) create mode 100644 docs/config/troubleshooting.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index a269be46b4..68ab18ea37 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -163,6 +163,7 @@ export default extendConfig( { text: 'Build', link: '/config/build' }, { text: 'Pack', link: '/config/pack' }, { text: 'Staged', link: '/config/staged' }, + { text: 'Troubleshooting', link: '/config/troubleshooting' }, ], }, ], diff --git a/docs/config/index.md b/docs/config/index.md index 8fc06cda22..810a406189 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -28,67 +28,4 @@ Vite+ extends the basic Vite configuration with these additions: - [`test`](/config/test) for Vitest - [`run`](/config/run) for Vite Task - [`pack`](/config/pack) for tsdown -- [`staged`](/config/staged) for staged-file checks - -## Lazy Loading Plugins - -When `vite.config.ts` imports heavy plugins at the top level, every `import` is evaluated eagerly, even for commands like `vp lint` or `vp fmt` that don't need those plugins. This can make config loading noticeably slow. - -The `lazy` field solves this by letting you defer plugin loading into an async function. Plugins provided through `lazy` are only resolved when actually needed: - -```ts -import { defineConfig } from 'vite-plus'; - -export default defineConfig({ - lazy: async () => { - const { default: myHeavyPlugins } = await import('./my-heavy-plugins'); - return { plugins: myHeavyPlugins }; - }, -}); -``` - -### Type Signature - -```ts -lazy?: () => Promise<{ - plugins?: Plugin[]; -}>; -``` - -### Merging with Existing Plugins - -Plugins returned from `lazy` are appended after any plugins already in the `plugins` array. This lets you keep lightweight plugins inline and defer only the expensive ones: - -```ts -import { defineConfig } from 'vite-plus'; -import lightPlugin from 'vite-plugin-light'; - -export default defineConfig({ - plugins: [lightPlugin()], - lazy: async () => { - const { default: heavyPlugin } = await import('vite-plugin-heavy'); - return { plugins: [heavyPlugin()] }; - }, -}); -``` - -The resulting plugin order is: `[lightPlugin(), heavyPlugin()]`. - -### Function Config - -`lazy` also works with function-style and async function-style configs: - -```ts -import { defineConfig } from 'vite-plus'; - -export default defineConfig(async () => ({ - lazy: async () => { - const { default: heavyPlugin } = await import('vite-plugin-heavy'); - return { plugins: [heavyPlugin()] }; - }, -})); -``` - -::: info -The `lazy` field is a temporary Vite+ extension. We plan to support this in upstream Vite in the future. -::: +- [`staged`](/config/staged) for staged-file checks \ No newline at end of file diff --git a/docs/config/troubleshooting.md b/docs/config/troubleshooting.md new file mode 100644 index 0000000000..1c5cc9dbc5 --- /dev/null +++ b/docs/config/troubleshooting.md @@ -0,0 +1,35 @@ +# Configuration Troubleshooting + +Use this page when your Vite+ configuration is not behaving the way you expect. + +## Slow config loading caused by heavy plugins + +When `vite.config.ts` imports heavy plugins at the top level, every `import` is evaluated eagerly, even for commands like `vp lint` or `vp fmt` that don't need those plugins. This can make config loading noticeably slow. + +Vite supports promises in the `plugins` array, so you can use dynamic `import()` to defer heavy plugin loading: + +```ts +import { defineConfig } from 'vite-plus'; + +export default defineConfig({ + plugins: [ + import('vite-plugin-heavy').then((m) => m.default()), + ], +}); +``` + +This way the plugin module is only loaded when Vite actually resolves plugins, keeping config loading fast for commands that don't need them. + +You can mix regular plugins with deferred ones. Lightweight plugins stay inline while expensive ones use dynamic `import()`: + +```ts +import { defineConfig } from 'vite-plus'; +import lightPlugin from 'vite-plugin-light'; + +export default defineConfig({ + plugins: [ + lightPlugin(), + import('vite-plugin-heavy').then((m) => m.default()), + ], +}); +``` diff --git a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt b/packages/cli/snap-tests/lazy-loading-plugins/snap.txt index 5ad97fc496..c57bbb843b 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt +++ b/packages/cli/snap-tests/lazy-loading-plugins/snap.txt @@ -1,4 +1,4 @@ -> # Test that plugins loaded via lazy field are applied during build +> # Test that lazy-loaded plugins via dynamic import are applied during build > vp build > cat dist/index.html | grep 'lazy-plugin-injected' diff --git a/packages/cli/snap-tests/lazy-loading-plugins/steps.json b/packages/cli/snap-tests/lazy-loading-plugins/steps.json index 8139fcae5f..07498c5609 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/steps.json +++ b/packages/cli/snap-tests/lazy-loading-plugins/steps.json @@ -1,6 +1,6 @@ { "commands": [ - "# Test that plugins loaded via lazy field are applied during build", + "# Test that lazy-loaded plugins via dynamic import are applied during build", { "command": "vp build", "ignoreOutput": true diff --git a/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts b/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts index 3875756cf2..5101d9f948 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts +++ b/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts @@ -1,8 +1,5 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ - lazy: async () => { - const { default: myLazyPlugin } = await import('./my-plugin'); - return { plugins: [myLazyPlugin()] }; - }, + plugins: [import('./my-plugin').then((m) => m.default())], }); From 8936d0c61d04facbc2d81130620d07dfcdb9bb7c Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 31 Mar 2026 11:30:21 +0800 Subject: [PATCH 3/3] docs: revert to lazy field approach for deferred plugin loading Vite does not support functions or async factories in the plugins array, so there is no native way to truly lazy-load plugins. Revert the troubleshooting docs and snap test to use the vite-plus lazy field. --- docs/config/troubleshooting.md | 28 +++++++++++-------- .../snap-tests/lazy-loading-plugins/snap.txt | 2 +- .../lazy-loading-plugins/steps.json | 2 +- .../lazy-loading-plugins/vite.config.ts | 5 +++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/config/troubleshooting.md b/docs/config/troubleshooting.md index 1c5cc9dbc5..0236017b43 100644 --- a/docs/config/troubleshooting.md +++ b/docs/config/troubleshooting.md @@ -6,30 +6,36 @@ Use this page when your Vite+ configuration is not behaving the way you expect. When `vite.config.ts` imports heavy plugins at the top level, every `import` is evaluated eagerly, even for commands like `vp lint` or `vp fmt` that don't need those plugins. This can make config loading noticeably slow. -Vite supports promises in the `plugins` array, so you can use dynamic `import()` to defer heavy plugin loading: +Use the `lazy` field in `defineConfig` to defer heavy plugin loading. Plugins provided through `lazy` are only resolved when Vite actually needs them: ```ts import { defineConfig } from 'vite-plus'; export default defineConfig({ - plugins: [ - import('vite-plugin-heavy').then((m) => m.default()), - ], + lazy: async () => { + const { default: heavyPlugin } = await import('vite-plugin-heavy'); + return { plugins: [heavyPlugin()] }; + }, }); ``` -This way the plugin module is only loaded when Vite actually resolves plugins, keeping config loading fast for commands that don't need them. - -You can mix regular plugins with deferred ones. Lightweight plugins stay inline while expensive ones use dynamic `import()`: +You can keep lightweight plugins inline and defer only the expensive ones. Plugins from `lazy` are appended after existing plugins: ```ts import { defineConfig } from 'vite-plus'; import lightPlugin from 'vite-plugin-light'; export default defineConfig({ - plugins: [ - lightPlugin(), - import('vite-plugin-heavy').then((m) => m.default()), - ], + plugins: [lightPlugin()], + lazy: async () => { + const { default: heavyPlugin } = await import('vite-plugin-heavy'); + return { plugins: [heavyPlugin()] }; + }, }); ``` + +The resulting plugin order is: `[lightPlugin(), heavyPlugin()]`. + +::: info +The `lazy` field is a Vite+ extension. We plan to support this in upstream Vite in the future. +::: diff --git a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt b/packages/cli/snap-tests/lazy-loading-plugins/snap.txt index c57bbb843b..5ad97fc496 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt +++ b/packages/cli/snap-tests/lazy-loading-plugins/snap.txt @@ -1,4 +1,4 @@ -> # Test that lazy-loaded plugins via dynamic import are applied during build +> # Test that plugins loaded via lazy field are applied during build > vp build > cat dist/index.html | grep 'lazy-plugin-injected' diff --git a/packages/cli/snap-tests/lazy-loading-plugins/steps.json b/packages/cli/snap-tests/lazy-loading-plugins/steps.json index 07498c5609..8139fcae5f 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/steps.json +++ b/packages/cli/snap-tests/lazy-loading-plugins/steps.json @@ -1,6 +1,6 @@ { "commands": [ - "# Test that lazy-loaded plugins via dynamic import are applied during build", + "# Test that plugins loaded via lazy field are applied during build", { "command": "vp build", "ignoreOutput": true diff --git a/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts b/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts index 5101d9f948..3875756cf2 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts +++ b/packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts @@ -1,5 +1,8 @@ import { defineConfig } from 'vite-plus'; export default defineConfig({ - plugins: [import('./my-plugin').then((m) => m.default())], + lazy: async () => { + const { default: myLazyPlugin } = await import('./my-plugin'); + return { plugins: [myLazyPlugin()] }; + }, });