From 96dfe1a6fca2659ceab927d093e14f02dbb7aec3 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 30 Mar 2026 20:31:23 +0800 Subject: [PATCH 01/14] 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 ca5757de8b..8ed063cc04 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 5e8b649d40df8ec42fa4bcde704c3d62fc4877d1 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 31 Mar 2026 11:07:47 +0800 Subject: [PATCH 02/14] 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 cc8352b4fb76db3deb1acaefdae6133fea397d28 Mon Sep 17 00:00:00 2001 From: MK Date: Tue, 31 Mar 2026 11:30:21 +0800 Subject: [PATCH 03/14] 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()] }; + }, }); From db99bd623745f04d59473323cb8fe411cb0d4609 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:24:10 +0800 Subject: [PATCH 04/14] feat: replace lazy field with vitePlugins() helper for conditional plugin loading Replace the `lazy` field approach in `defineConfig` with a standalone `vitePlugins()` function that checks the `VP_COMMAND` environment variable to conditionally load plugins only for commands that need them (dev, build, test, preview). This is simpler, more explicit, and doesn't require modifying defineConfig internals. The VP_COMMAND env var is set automatically by vp for every command. Refs https://github.com/vitejs/vite/issues/22085 --- docs/config/troubleshooting.md | 25 +- .../lazy-loading-plugins/vite.config.ts | 8 +- packages/cli/src/__tests__/index.spec.ts | 236 +++++------------- packages/cli/src/bin.ts | 1 + packages/cli/src/define-config.ts | 57 +---- packages/cli/src/index.cts | 3 +- packages/cli/src/index.ts | 4 +- 7 files changed, 84 insertions(+), 250 deletions(-) diff --git a/docs/config/troubleshooting.md b/docs/config/troubleshooting.md index 0236017b43..11b0d9edb7 100644 --- a/docs/config/troubleshooting.md +++ b/docs/config/troubleshooting.md @@ -6,36 +6,31 @@ 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. -Use the `lazy` field in `defineConfig` to defer heavy plugin loading. Plugins provided through `lazy` are only resolved when Vite actually needs them: +Use the `vitePlugins()` helper to conditionally load plugins. It checks which `vp` command is running and skips plugin loading for commands that don't need them (like `lint`, `fmt`, `check`): ```ts -import { defineConfig } from 'vite-plus'; +import { defineConfig, vitePlugins } from 'vite-plus'; export default defineConfig({ - lazy: async () => { - const { default: heavyPlugin } = await import('vite-plugin-heavy'); - return { plugins: [heavyPlugin()] }; - }, + plugins: vitePlugins(() => [myPlugin()]), }); ``` -You can keep lightweight plugins inline and defer only the expensive ones. Plugins from `lazy` are appended after existing plugins: +For heavy plugins that should be lazily imported, combine with dynamic `import()`: ```ts -import { defineConfig } from 'vite-plus'; -import lightPlugin from 'vite-plugin-light'; +import { defineConfig, vitePlugins } from 'vite-plus'; export default defineConfig({ - plugins: [lightPlugin()], - lazy: async () => { + plugins: vitePlugins(async () => { const { default: heavyPlugin } = await import('vite-plugin-heavy'); - return { plugins: [heavyPlugin()] }; - }, + return [heavyPlugin()]; + }), }); ``` -The resulting plugin order is: `[lightPlugin(), heavyPlugin()]`. +Plugins load for `dev`, `build`, `test`, and `preview`. They are skipped for `lint`, `fmt`, `check`, and other commands that don't need them. ::: info -The `lazy` field is a Vite+ extension. We plan to support this in upstream Vite in the future. +`vitePlugins()` works by checking the `VP_COMMAND` environment variable, which is automatically set by `vp` for every command. ::: 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..84126256e9 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,8 @@ -import { defineConfig } from 'vite-plus'; +import { defineConfig, vitePlugins } from 'vite-plus'; export default defineConfig({ - lazy: async () => { + plugins: vitePlugins(async () => { const { default: myLazyPlugin } = await import('./my-plugin'); - return { plugins: [myLazyPlugin()] }; - }, + return [myLazyPlugin()]; + }), }); diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts index 8ed063cc04..cd164ec896 100644 --- a/packages/cli/src/__tests__/index.spec.ts +++ b/packages/cli/src/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '@voidzero-dev/vite-plus-test'; +import { afterEach, beforeEach, expect, test } from '@voidzero-dev/vite-plus-test'; import { configDefaults, @@ -8,11 +8,27 @@ import { defaultBrowserPort, defineConfig, defineProject, + vitePlugins, } from '../index.js'; +let originalVpCommand: string | undefined; + +beforeEach(() => { + originalVpCommand = process.env.VP_COMMAND; +}); + +afterEach(() => { + if (originalVpCommand === undefined) { + delete process.env.VP_COMMAND; + } else { + process.env.VP_COMMAND = originalVpCommand; + } +}); + test('should keep vitest exports stable', () => { expect(defineConfig).toBeTypeOf('function'); expect(defineProject).toBeTypeOf('function'); + expect(vitePlugins).toBeTypeOf('function'); expect(configDefaults).toBeDefined(); expect(coverageConfigDefaults).toBeDefined(); expect(defaultExclude).toBeDefined(); @@ -20,184 +36,50 @@ test('should keep vitest exports stable', () => { expect(defaultBrowserPort).toBeDefined(); }); -test('should support lazy loading of plugins', async () => { - const config = await defineConfig({ - lazy: () => Promise.resolve({ plugins: [{ name: 'test' }] }), - }); - expect(config.plugins?.length).toBe(1); -}); - -test('should merge lazy plugins with existing plugins', async () => { - const config = await defineConfig({ - plugins: [{ name: 'existing' }], - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }), - }); - 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('lazy'); -}); - -test('should handle lazy with empty plugins array', async () => { - const config = await defineConfig({ - lazy: () => Promise.resolve({ plugins: [] }), - }); - expect(config.plugins?.length).toBe(0); -}); - -test('should handle lazy returning undefined plugins', async () => { - const config = await defineConfig({ - lazy: () => Promise.resolve({}), - }); - expect(config.plugins?.length).toBe(0); -}); - -test('should handle Promise config with lazy', async () => { - const config = await defineConfig( - Promise.resolve({ - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-promise' }] }), - }), - ); - expect(config.plugins?.length).toBe(1); - expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-promise'); -}); - -test('should handle Promise config with lazy and existing plugins', async () => { - const config = await defineConfig( - Promise.resolve({ - plugins: [{ name: 'existing' }], - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }), - }), - ); - 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('lazy'); -}); - -test('should handle Promise config without lazy', async () => { - const config = await defineConfig( - Promise.resolve({ - plugins: [{ name: 'no-lazy' }], - }), - ); - expect(config.plugins?.length).toBe(1); - expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy'); -}); - -test('should handle function config with lazy', async () => { - const configFn = defineConfig(() => ({ - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-fn' }] }), - })); - expect(typeof configFn).toBe('function'); - const config = await configFn({ command: 'build', mode: 'production' }); - expect(config.plugins?.length).toBe(1); - expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-fn'); -}); - -test('should handle function config with lazy and existing plugins', async () => { - const configFn = defineConfig(() => ({ - plugins: [{ name: 'existing' }], - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }), - })); - const config = await configFn({ command: 'build', mode: 'production' }); - 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('lazy'); -}); - -test('should handle function config without lazy', () => { - const configFn = defineConfig(() => ({ - plugins: [{ name: 'no-lazy' }], - })); - const config = configFn({ command: 'build', mode: 'production' }); - expect(config.plugins?.length).toBe(1); - expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy'); -}); - -test('should handle async function config with lazy', async () => { - const configFn = defineConfig(async () => ({ - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-async-fn' }] }), - })); - const config = await configFn({ command: 'build', mode: 'production' }); - expect(config.plugins?.length).toBe(1); - expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-async-fn'); -}); - -test('should handle async function config with lazy and existing plugins', async () => { - const configFn = defineConfig(async () => ({ - plugins: [{ name: 'existing' }], - lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }), - })); - const config = await configFn({ command: 'build', mode: 'production' }); - 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('lazy'); -}); - -test('should handle async function config without lazy', async () => { - const configFn = defineConfig(async () => ({ - plugins: [{ name: 'no-lazy' }], - })); - const config = await configFn({ command: 'build', mode: 'production' }); - 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 }; - }, +test('vitePlugins returns undefined when VP_COMMAND is unset', () => { + delete process.env.VP_COMMAND; + const result = vitePlugins(() => [{ name: 'test' }]); + expect(result).toBeUndefined(); +}); + +test('vitePlugins returns undefined when VP_COMMAND is empty string', () => { + process.env.VP_COMMAND = ''; + const result = vitePlugins(() => [{ name: 'test' }]); + expect(result).toBeUndefined(); +}); + +test.each(['dev', 'build', 'test', 'preview'])( + 'vitePlugins executes callback when VP_COMMAND is %s', + (cmd) => { + process.env.VP_COMMAND = cmd; + const result = vitePlugins(() => [{ name: 'my-plugin' }]); + expect(result).toEqual([{ name: 'my-plugin' }]); + }, +); + +test.each(['lint', 'fmt', 'check', 'pack', 'install', 'run'])( + 'vitePlugins returns undefined when VP_COMMAND is %s', + (cmd) => { + process.env.VP_COMMAND = cmd; + const result = vitePlugins(() => [{ name: 'my-plugin' }]); + expect(result).toBeUndefined(); + }, +); + +test('vitePlugins supports async callback', async () => { + process.env.VP_COMMAND = 'build'; + const result = vitePlugins(async () => { + const plugin = await Promise.resolve({ name: 'async-plugin' }); + return [plugin]; }); - 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'); + expect(result).toBeInstanceOf(Promise); + expect(await result).toEqual([{ name: 'async-plugin' }]); }); -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] }; - }, +test('vitePlugins returns undefined for async callback when skipped', () => { + process.env.VP_COMMAND = 'lint'; + const result = vitePlugins(async () => { + return [{ name: 'async-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'); + expect(result).toBeUndefined(); }); diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 056e4c6846..5aecc7905e 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -45,6 +45,7 @@ if (args[0] === 'help' && args[1]) { } const command = args[0]; +process.env.VP_COMMAND = command ?? ''; // Global commands — handled by tsdown-bundled modules in dist/ if (command === 'create') { diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index 6b5a8a8acd..cb52fa88df 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -2,7 +2,6 @@ import type { UserConfig } from '@voidzero-dev/vite-plus-core'; import { defineConfig as viteDefineConfig, type ConfigEnv, - type Plugin as VitestPlugin, } from '@voidzero-dev/vite-plus-test/config'; import type { OxfmtConfig } from 'oxfmt'; import type { OxlintConfig } from 'oxlint'; @@ -25,12 +24,6 @@ declare module '@voidzero-dev/vite-plus-core' { run?: RunConfig; staged?: StagedConfig; - - // temporary solution to load plugins lazily - // We need to support this in the upstream vite - lazy?: () => Promise<{ - plugins?: VitestPlugin[]; - }>; } } @@ -51,49 +44,11 @@ export function defineConfig(config: ViteUserConfigFnPromise): ViteUserConfigFnP export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport; export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport { - if (typeof config === 'object') { - if (config instanceof Promise) { - return config.then((config) => { - if (config.lazy) { - return config.lazy().then(({ plugins }) => - viteDefineConfig({ - ...config, - plugins: [...(config.plugins || []), ...(plugins || [])], - }), - ); - } - return viteDefineConfig(config); - }); - } else if (config.lazy) { - return config.lazy().then(({ plugins }) => - viteDefineConfig({ - ...config, - plugins: [...(config.plugins || []), ...(plugins || [])], - }), - ); - } - } else if (typeof config === 'function') { - return viteDefineConfig((env) => { - const c = config(env); - if (c instanceof Promise) { - return c.then((v) => { - if (v.lazy) { - return v - .lazy() - .then(({ plugins }) => - viteDefineConfig({ ...v, plugins: [...(v.plugins || []), ...(plugins || [])] }), - ); - } - return v; - }); - } - if (c.lazy) { - return c - .lazy() - .then(({ plugins }) => ({ ...c, plugins: [...(c.plugins || []), ...(plugins || [])] })); - } - return c; - }); - } return viteDefineConfig(config); } + +export function vitePlugins(cb: () => T): T | undefined { + const cmd = process.env.VP_COMMAND; + if (cmd === 'dev' || cmd === 'build' || cmd === 'test' || cmd === 'preview') return cb(); + return undefined; +} diff --git a/packages/cli/src/index.cts b/packages/cli/src/index.cts index 3e28da3464..18d92dd90c 100644 --- a/packages/cli/src/index.cts +++ b/packages/cli/src/index.cts @@ -2,10 +2,11 @@ const vite = require('@voidzero-dev/vite-plus-core'); const vitest = require('@voidzero-dev/vite-plus-test/config'); -const { defineConfig } = require('./define-config'); +const { defineConfig, vitePlugins } = require('./define-config'); module.exports = { ...vite, ...vitest, defineConfig, + vitePlugins, }; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 183cd8872f..63fa50c9b4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,7 +1,7 @@ -import { defineConfig } from './define-config.ts'; +import { defineConfig, vitePlugins } from './define-config.ts'; export * from '@voidzero-dev/vite-plus-core'; export * from '@voidzero-dev/vite-plus-test/config'; -export { defineConfig }; +export { defineConfig, vitePlugins }; From 71d3659ef6ae91e20efaa510eb2d8a94d516d4db Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:25:07 +0800 Subject: [PATCH 05/14] fix: add curly braces to satisfy eslint curly rule --- packages/cli/src/define-config.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index cb52fa88df..a93c478a76 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -49,6 +49,8 @@ export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport export function vitePlugins(cb: () => T): T | undefined { const cmd = process.env.VP_COMMAND; - if (cmd === 'dev' || cmd === 'build' || cmd === 'test' || cmd === 'preview') return cb(); + if (cmd === 'dev' || cmd === 'build' || cmd === 'test' || cmd === 'preview') { + return cb(); + } return undefined; } From 4a6bc991948905c8c780b2b5f9314ff66dd2b9a6 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:31:09 +0800 Subject: [PATCH 06/14] fix: return empty array instead of undefined and wrap async in array vitePlugins() now returns [] when skipping, making it directly assignable to plugins. For async callbacks, wrap in an array since Vite's plugins field expects PluginOption[], not a bare Promise. --- docs/config/troubleshooting.md | 10 ++++++---- .../lazy-loading-plugins/vite.config.ts | 10 ++++++---- packages/cli/src/__tests__/index.spec.ts | 16 ++++++++-------- packages/cli/src/define-config.ts | 4 ++-- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/config/troubleshooting.md b/docs/config/troubleshooting.md index 11b0d9edb7..f0b1b87a8b 100644 --- a/docs/config/troubleshooting.md +++ b/docs/config/troubleshooting.md @@ -22,10 +22,12 @@ For heavy plugins that should be lazily imported, combine with dynamic `import() import { defineConfig, vitePlugins } from 'vite-plus'; export default defineConfig({ - plugins: vitePlugins(async () => { - const { default: heavyPlugin } = await import('vite-plugin-heavy'); - return [heavyPlugin()]; - }), + plugins: [ + vitePlugins(async () => { + const { default: heavyPlugin } = await import('vite-plugin-heavy'); + return [heavyPlugin()]; + }), + ], }); ``` 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 84126256e9..ced83ee820 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,10 @@ import { defineConfig, vitePlugins } from 'vite-plus'; export default defineConfig({ - plugins: vitePlugins(async () => { - const { default: myLazyPlugin } = await import('./my-plugin'); - return [myLazyPlugin()]; - }), + plugins: [ + vitePlugins(async () => { + const { default: myLazyPlugin } = await import('./my-plugin'); + return [myLazyPlugin()]; + }), + ], }); diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts index cd164ec896..f270adb6b1 100644 --- a/packages/cli/src/__tests__/index.spec.ts +++ b/packages/cli/src/__tests__/index.spec.ts @@ -36,16 +36,16 @@ test('should keep vitest exports stable', () => { expect(defaultBrowserPort).toBeDefined(); }); -test('vitePlugins returns undefined when VP_COMMAND is unset', () => { +test('vitePlugins returns empty array when VP_COMMAND is unset', () => { delete process.env.VP_COMMAND; const result = vitePlugins(() => [{ name: 'test' }]); - expect(result).toBeUndefined(); + expect(result).toEqual([]); }); -test('vitePlugins returns undefined when VP_COMMAND is empty string', () => { +test('vitePlugins returns empty array when VP_COMMAND is empty string', () => { process.env.VP_COMMAND = ''; const result = vitePlugins(() => [{ name: 'test' }]); - expect(result).toBeUndefined(); + expect(result).toEqual([]); }); test.each(['dev', 'build', 'test', 'preview'])( @@ -58,11 +58,11 @@ test.each(['dev', 'build', 'test', 'preview'])( ); test.each(['lint', 'fmt', 'check', 'pack', 'install', 'run'])( - 'vitePlugins returns undefined when VP_COMMAND is %s', + 'vitePlugins returns empty array when VP_COMMAND is %s', (cmd) => { process.env.VP_COMMAND = cmd; const result = vitePlugins(() => [{ name: 'my-plugin' }]); - expect(result).toBeUndefined(); + expect(result).toEqual([]); }, ); @@ -76,10 +76,10 @@ test('vitePlugins supports async callback', async () => { expect(await result).toEqual([{ name: 'async-plugin' }]); }); -test('vitePlugins returns undefined for async callback when skipped', () => { +test('vitePlugins returns empty array for async callback when skipped', () => { process.env.VP_COMMAND = 'lint'; const result = vitePlugins(async () => { return [{ name: 'async-plugin' }]; }); - expect(result).toBeUndefined(); + expect(result).toEqual([]); }); diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index a93c478a76..ae307665d6 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -47,10 +47,10 @@ export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport return viteDefineConfig(config); } -export function vitePlugins(cb: () => T): T | undefined { +export function vitePlugins(cb: () => T): T | [] { const cmd = process.env.VP_COMMAND; if (cmd === 'dev' || cmd === 'build' || cmd === 'test' || cmd === 'preview') { return cb(); } - return undefined; + return []; } From df26b2746af0339fb24fbc0e3cfcc9cfc753162a Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:32:35 +0800 Subject: [PATCH 07/14] fix: revert vitePlugins return type to T | undefined --- packages/cli/src/__tests__/index.spec.ts | 16 ++++++++-------- packages/cli/src/define-config.ts | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts index f270adb6b1..cd164ec896 100644 --- a/packages/cli/src/__tests__/index.spec.ts +++ b/packages/cli/src/__tests__/index.spec.ts @@ -36,16 +36,16 @@ test('should keep vitest exports stable', () => { expect(defaultBrowserPort).toBeDefined(); }); -test('vitePlugins returns empty array when VP_COMMAND is unset', () => { +test('vitePlugins returns undefined when VP_COMMAND is unset', () => { delete process.env.VP_COMMAND; const result = vitePlugins(() => [{ name: 'test' }]); - expect(result).toEqual([]); + expect(result).toBeUndefined(); }); -test('vitePlugins returns empty array when VP_COMMAND is empty string', () => { +test('vitePlugins returns undefined when VP_COMMAND is empty string', () => { process.env.VP_COMMAND = ''; const result = vitePlugins(() => [{ name: 'test' }]); - expect(result).toEqual([]); + expect(result).toBeUndefined(); }); test.each(['dev', 'build', 'test', 'preview'])( @@ -58,11 +58,11 @@ test.each(['dev', 'build', 'test', 'preview'])( ); test.each(['lint', 'fmt', 'check', 'pack', 'install', 'run'])( - 'vitePlugins returns empty array when VP_COMMAND is %s', + 'vitePlugins returns undefined when VP_COMMAND is %s', (cmd) => { process.env.VP_COMMAND = cmd; const result = vitePlugins(() => [{ name: 'my-plugin' }]); - expect(result).toEqual([]); + expect(result).toBeUndefined(); }, ); @@ -76,10 +76,10 @@ test('vitePlugins supports async callback', async () => { expect(await result).toEqual([{ name: 'async-plugin' }]); }); -test('vitePlugins returns empty array for async callback when skipped', () => { +test('vitePlugins returns undefined for async callback when skipped', () => { process.env.VP_COMMAND = 'lint'; const result = vitePlugins(async () => { return [{ name: 'async-plugin' }]; }); - expect(result).toEqual([]); + expect(result).toBeUndefined(); }); diff --git a/packages/cli/src/define-config.ts b/packages/cli/src/define-config.ts index ae307665d6..a93c478a76 100644 --- a/packages/cli/src/define-config.ts +++ b/packages/cli/src/define-config.ts @@ -47,10 +47,10 @@ export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport return viteDefineConfig(config); } -export function vitePlugins(cb: () => T): T | [] { +export function vitePlugins(cb: () => T): T | undefined { const cmd = process.env.VP_COMMAND; if (cmd === 'dev' || cmd === 'build' || cmd === 'test' || cmd === 'preview') { return cb(); } - return []; + return undefined; } From df1e199a680fd758c9a83de78f18a6aae6e5b9ff Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:35:02 +0800 Subject: [PATCH 08/14] test: add snap test for sync vitePlugins() usage --- packages/cli/snap-tests/vite-plugins-sync/index.html | 8 ++++++++ packages/cli/snap-tests/vite-plugins-sync/my-plugin.ts | 8 ++++++++ packages/cli/snap-tests/vite-plugins-sync/package.json | 4 ++++ packages/cli/snap-tests/vite-plugins-sync/snap.txt | 4 ++++ packages/cli/snap-tests/vite-plugins-sync/steps.json | 10 ++++++++++ .../cli/snap-tests/vite-plugins-sync/vite.config.ts | 6 ++++++ 6 files changed, 40 insertions(+) create mode 100644 packages/cli/snap-tests/vite-plugins-sync/index.html create mode 100644 packages/cli/snap-tests/vite-plugins-sync/my-plugin.ts create mode 100644 packages/cli/snap-tests/vite-plugins-sync/package.json create mode 100644 packages/cli/snap-tests/vite-plugins-sync/snap.txt create mode 100644 packages/cli/snap-tests/vite-plugins-sync/steps.json create mode 100644 packages/cli/snap-tests/vite-plugins-sync/vite.config.ts diff --git a/packages/cli/snap-tests/vite-plugins-sync/index.html b/packages/cli/snap-tests/vite-plugins-sync/index.html new file mode 100644 index 0000000000..7febab8c7e --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/index.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/cli/snap-tests/vite-plugins-sync/my-plugin.ts b/packages/cli/snap-tests/vite-plugins-sync/my-plugin.ts new file mode 100644 index 0000000000..dc921e8186 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/my-plugin.ts @@ -0,0 +1,8 @@ +export default function mySyncPlugin() { + return { + name: 'my-sync-plugin', + transformIndexHtml(html: string) { + return html.replace('', ''); + }, + }; +} diff --git a/packages/cli/snap-tests/vite-plugins-sync/package.json b/packages/cli/snap-tests/vite-plugins-sync/package.json new file mode 100644 index 0000000000..ea51c4e4e1 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/package.json @@ -0,0 +1,4 @@ +{ + "name": "vite-plugins-sync-test", + "private": true +} diff --git a/packages/cli/snap-tests/vite-plugins-sync/snap.txt b/packages/cli/snap-tests/vite-plugins-sync/snap.txt new file mode 100644 index 0000000000..b8fbd77851 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/snap.txt @@ -0,0 +1,4 @@ +> # Test that plugins loaded via sync vitePlugins() are applied during build +> vp build +> cat dist/index.html | grep 'sync-plugin-injected' + diff --git a/packages/cli/snap-tests/vite-plugins-sync/steps.json b/packages/cli/snap-tests/vite-plugins-sync/steps.json new file mode 100644 index 0000000000..133d48c750 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/steps.json @@ -0,0 +1,10 @@ +{ + "commands": [ + "# Test that plugins loaded via sync vitePlugins() are applied during build", + { + "command": "vp build", + "ignoreOutput": true + }, + "cat dist/index.html | grep 'sync-plugin-injected'" + ] +} diff --git a/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts new file mode 100644 index 0000000000..64a9f09b18 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig, vitePlugins } from 'vite-plus'; +import mySyncPlugin from './my-plugin'; + +export default defineConfig({ + plugins: vitePlugins(() => [mySyncPlugin()]), +}); From c6a609e08901f86de55626150db9cf95d9e3463a Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:35:44 +0800 Subject: [PATCH 09/14] refactor: rename lazy-loading-plugins snap test to vite-plugins-async --- .../{lazy-loading-plugins => vite-plugins-async}/index.html | 0 .../{lazy-loading-plugins => vite-plugins-async}/my-plugin.ts | 0 .../{lazy-loading-plugins => vite-plugins-async}/package.json | 0 .../{lazy-loading-plugins => vite-plugins-async}/snap.txt | 2 +- .../{lazy-loading-plugins => vite-plugins-async}/steps.json | 2 +- .../{lazy-loading-plugins => vite-plugins-async}/vite.config.ts | 0 packages/cli/snap-tests/vite-plugins-sync/vite.config.ts | 1 + 7 files changed, 3 insertions(+), 2 deletions(-) rename packages/cli/snap-tests/{lazy-loading-plugins => vite-plugins-async}/index.html (100%) rename packages/cli/snap-tests/{lazy-loading-plugins => vite-plugins-async}/my-plugin.ts (100%) rename packages/cli/snap-tests/{lazy-loading-plugins => vite-plugins-async}/package.json (100%) rename packages/cli/snap-tests/{lazy-loading-plugins => vite-plugins-async}/snap.txt (56%) rename packages/cli/snap-tests/{lazy-loading-plugins => vite-plugins-async}/steps.json (64%) rename packages/cli/snap-tests/{lazy-loading-plugins => vite-plugins-async}/vite.config.ts (100%) diff --git a/packages/cli/snap-tests/lazy-loading-plugins/index.html b/packages/cli/snap-tests/vite-plugins-async/index.html similarity index 100% rename from packages/cli/snap-tests/lazy-loading-plugins/index.html rename to packages/cli/snap-tests/vite-plugins-async/index.html diff --git a/packages/cli/snap-tests/lazy-loading-plugins/my-plugin.ts b/packages/cli/snap-tests/vite-plugins-async/my-plugin.ts similarity index 100% rename from packages/cli/snap-tests/lazy-loading-plugins/my-plugin.ts rename to packages/cli/snap-tests/vite-plugins-async/my-plugin.ts diff --git a/packages/cli/snap-tests/lazy-loading-plugins/package.json b/packages/cli/snap-tests/vite-plugins-async/package.json similarity index 100% rename from packages/cli/snap-tests/lazy-loading-plugins/package.json rename to packages/cli/snap-tests/vite-plugins-async/package.json diff --git a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt b/packages/cli/snap-tests/vite-plugins-async/snap.txt similarity index 56% rename from packages/cli/snap-tests/lazy-loading-plugins/snap.txt rename to packages/cli/snap-tests/vite-plugins-async/snap.txt index 5ad97fc496..d37d55ef72 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/snap.txt +++ b/packages/cli/snap-tests/vite-plugins-async/snap.txt @@ -1,4 +1,4 @@ -> # Test that plugins loaded via lazy field are applied during build +> # Test that plugins loaded via async vitePlugins() 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/vite-plugins-async/steps.json similarity index 64% rename from packages/cli/snap-tests/lazy-loading-plugins/steps.json rename to packages/cli/snap-tests/vite-plugins-async/steps.json index 8139fcae5f..6d3c0fd185 100644 --- a/packages/cli/snap-tests/lazy-loading-plugins/steps.json +++ b/packages/cli/snap-tests/vite-plugins-async/steps.json @@ -1,6 +1,6 @@ { "commands": [ - "# Test that plugins loaded via lazy field are applied during build", + "# Test that plugins loaded via async vitePlugins() 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/vite-plugins-async/vite.config.ts similarity index 100% rename from packages/cli/snap-tests/lazy-loading-plugins/vite.config.ts rename to packages/cli/snap-tests/vite-plugins-async/vite.config.ts diff --git a/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts index 64a9f09b18..69a8aa97b8 100644 --- a/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts +++ b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig, vitePlugins } from 'vite-plus'; + import mySyncPlugin from './my-plugin'; export default defineConfig({ From 31f920c75ce6a90b6a7e43bfe3e27145b3aac0e2 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 15:38:36 +0800 Subject: [PATCH 10/14] docs: use consistent array-wrapped syntax for both sync and async vitePlugins --- docs/config/troubleshooting.md | 6 +++++- packages/cli/snap-tests/vite-plugins-sync/vite.config.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/config/troubleshooting.md b/docs/config/troubleshooting.md index f0b1b87a8b..0abdbbf2e1 100644 --- a/docs/config/troubleshooting.md +++ b/docs/config/troubleshooting.md @@ -11,8 +11,12 @@ Use the `vitePlugins()` helper to conditionally load plugins. It checks which `v ```ts import { defineConfig, vitePlugins } from 'vite-plus'; +import myPlugin from 'vite-plugin-foo'; + export default defineConfig({ - plugins: vitePlugins(() => [myPlugin()]), + plugins: [ + vitePlugins(() => [myPlugin()]), + ], }); ``` diff --git a/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts index 69a8aa97b8..756e9b2d24 100644 --- a/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts +++ b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts @@ -3,5 +3,5 @@ import { defineConfig, vitePlugins } from 'vite-plus'; import mySyncPlugin from './my-plugin'; export default defineConfig({ - plugins: vitePlugins(() => [mySyncPlugin()]), + plugins: [vitePlugins(() => [mySyncPlugin()])], }); From 52fd15b0909c284abfc59d4f8a5bad453595e59c Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 16:14:03 +0800 Subject: [PATCH 11/14] test: add snap test to verify vp lint skips plugin loading --- .../snap-tests/vite-plugins-skip-on-lint/package.json | 4 ++++ .../cli/snap-tests/vite-plugins-skip-on-lint/snap.txt | 4 ++++ .../snap-tests/vite-plugins-skip-on-lint/src/index.ts | 1 + .../cli/snap-tests/vite-plugins-skip-on-lint/steps.json | 6 ++++++ .../snap-tests/vite-plugins-skip-on-lint/vite.config.ts | 9 +++++++++ 5 files changed, 24 insertions(+) create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-lint/package.json create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-lint/src/index.ts create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/package.json b/packages/cli/snap-tests/vite-plugins-skip-on-lint/package.json new file mode 100644 index 0000000000..9212954e86 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/package.json @@ -0,0 +1,4 @@ +{ + "name": "vite-plugins-skip-on-lint-test", + "private": true +} diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt b/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt new file mode 100644 index 0000000000..a6c0f9c6a1 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt @@ -0,0 +1,4 @@ +> # Test that vp lint does not load plugins (the plugin throws if loaded) +> vp lint src/ +Found 0 warnings and 0 errors. +Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/src/index.ts b/packages/cli/snap-tests/vite-plugins-skip-on-lint/src/index.ts new file mode 100644 index 0000000000..c155820bf7 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/src/index.ts @@ -0,0 +1 @@ +export const foo = 'bar'; diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json b/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json new file mode 100644 index 0000000000..607085251c --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json @@ -0,0 +1,6 @@ +{ + "commands": [ + "# Test that vp lint does not load plugins (the plugin throws if loaded)", + "vp lint src/" + ] +} diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts b/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts new file mode 100644 index 0000000000..56cbd6ecac --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig, vitePlugins } from 'vite-plus'; + +export default defineConfig({ + plugins: [ + vitePlugins(() => { + throw new Error('Plugins should not be loaded during lint'); + }), + ], +}); From 45c482760bbc0ec6933d5ff0827ca1263f443b4b Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 16:15:51 +0800 Subject: [PATCH 12/14] test: use async import pattern in skip-on-lint snap test The heavy-plugin.ts throws on import, proving the dynamic import inside vitePlugins() is never executed during vp lint. --- .../cli/snap-tests/vite-plugins-skip-on-lint/heavy-plugin.ts | 5 +++++ .../cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 packages/cli/snap-tests/vite-plugins-skip-on-lint/heavy-plugin.ts diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/heavy-plugin.ts b/packages/cli/snap-tests/vite-plugins-skip-on-lint/heavy-plugin.ts new file mode 100644 index 0000000000..970cc641d0 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/heavy-plugin.ts @@ -0,0 +1,5 @@ +throw new Error('Plugins should not be loaded during lint'); + +export default function heavyPlugin() { + return { name: 'heavy-plugin' }; +} diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts b/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts index 56cbd6ecac..ef1451260a 100644 --- a/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts @@ -2,8 +2,9 @@ import { defineConfig, vitePlugins } from 'vite-plus'; export default defineConfig({ plugins: [ - vitePlugins(() => { - throw new Error('Plugins should not be loaded during lint'); + vitePlugins(async () => { + const { default: heavyPlugin } = await import('./heavy-plugin'); + return [heavyPlugin()]; }), ], }); From 93586a8e0d3396d93e5a532921bb0c277e316d2b Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 16:34:17 +0800 Subject: [PATCH 13/14] fix: move snap test comment inline after command --- packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt | 3 +-- packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt b/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt index a6c0f9c6a1..19f530d162 100644 --- a/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt @@ -1,4 +1,3 @@ -> # Test that vp lint does not load plugins (the plugin throws if loaded) -> vp lint src/ +> vp lint src/ # vp lint should not load plugins (heavy-plugin.ts throws if imported) Found 0 warnings and 0 errors. Finished in ms on 1 file with rules using threads. diff --git a/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json b/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json index 607085251c..1107efd99f 100644 --- a/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json @@ -1,6 +1,5 @@ { "commands": [ - "# Test that vp lint does not load plugins (the plugin throws if loaded)", - "vp lint src/" + "vp lint src/ # vp lint should not load plugins (heavy-plugin.ts throws if imported)" ] } From dfaae49cb186205a734f1fa9513b256c161799c5 Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 3 Apr 2026 16:34:46 +0800 Subject: [PATCH 14/14] fix: move snap test comments inline after commands --- packages/cli/snap-tests/vite-plugins-async/snap.txt | 3 +-- packages/cli/snap-tests/vite-plugins-async/steps.json | 3 +-- packages/cli/snap-tests/vite-plugins-sync/snap.txt | 3 +-- packages/cli/snap-tests/vite-plugins-sync/steps.json | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/cli/snap-tests/vite-plugins-async/snap.txt b/packages/cli/snap-tests/vite-plugins-async/snap.txt index d37d55ef72..5415a2e9e2 100644 --- a/packages/cli/snap-tests/vite-plugins-async/snap.txt +++ b/packages/cli/snap-tests/vite-plugins-async/snap.txt @@ -1,4 +1,3 @@ -> # Test that plugins loaded via async vitePlugins() are applied during build > vp build -> cat dist/index.html | grep 'lazy-plugin-injected' +> cat dist/index.html | grep 'lazy-plugin-injected' # async vitePlugins() should apply plugins during build diff --git a/packages/cli/snap-tests/vite-plugins-async/steps.json b/packages/cli/snap-tests/vite-plugins-async/steps.json index 6d3c0fd185..8c76d60634 100644 --- a/packages/cli/snap-tests/vite-plugins-async/steps.json +++ b/packages/cli/snap-tests/vite-plugins-async/steps.json @@ -1,10 +1,9 @@ { "commands": [ - "# Test that plugins loaded via async vitePlugins() are applied during build", { "command": "vp build", "ignoreOutput": true }, - "cat dist/index.html | grep 'lazy-plugin-injected'" + "cat dist/index.html | grep 'lazy-plugin-injected' # async vitePlugins() should apply plugins during build" ] } diff --git a/packages/cli/snap-tests/vite-plugins-sync/snap.txt b/packages/cli/snap-tests/vite-plugins-sync/snap.txt index b8fbd77851..82865c8566 100644 --- a/packages/cli/snap-tests/vite-plugins-sync/snap.txt +++ b/packages/cli/snap-tests/vite-plugins-sync/snap.txt @@ -1,4 +1,3 @@ -> # Test that plugins loaded via sync vitePlugins() are applied during build > vp build -> cat dist/index.html | grep 'sync-plugin-injected' +> cat dist/index.html | grep 'sync-plugin-injected' # sync vitePlugins() should apply plugins during build diff --git a/packages/cli/snap-tests/vite-plugins-sync/steps.json b/packages/cli/snap-tests/vite-plugins-sync/steps.json index 133d48c750..1309b02cdc 100644 --- a/packages/cli/snap-tests/vite-plugins-sync/steps.json +++ b/packages/cli/snap-tests/vite-plugins-sync/steps.json @@ -1,10 +1,9 @@ { "commands": [ - "# Test that plugins loaded via sync vitePlugins() are applied during build", { "command": "vp build", "ignoreOutput": true }, - "cat dist/index.html | grep 'sync-plugin-injected'" + "cat dist/index.html | grep 'sync-plugin-injected' # sync vitePlugins() should apply plugins during build" ] }