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 78807ce104..810a406189 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -28,4 +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 +- [`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..0abdbbf2e1 --- /dev/null +++ b/docs/config/troubleshooting.md @@ -0,0 +1,42 @@ +# 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. + +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, vitePlugins } from 'vite-plus'; + +import myPlugin from 'vite-plugin-foo'; + +export default defineConfig({ + plugins: [ + vitePlugins(() => [myPlugin()]), + ], +}); +``` + +For heavy plugins that should be lazily imported, combine with dynamic `import()`: + +```ts +import { defineConfig, vitePlugins } from 'vite-plus'; + +export default defineConfig({ + plugins: [ + vitePlugins(async () => { + const { default: heavyPlugin } = await import('vite-plugin-heavy'); + return [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 +`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/vite-plugins-async/index.html b/packages/cli/snap-tests/vite-plugins-async/index.html new file mode 100644 index 0000000000..7febab8c7e --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-async/index.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/cli/snap-tests/vite-plugins-async/my-plugin.ts b/packages/cli/snap-tests/vite-plugins-async/my-plugin.ts new file mode 100644 index 0000000000..33ba10fcfe --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-async/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/vite-plugins-async/package.json b/packages/cli/snap-tests/vite-plugins-async/package.json new file mode 100644 index 0000000000..be4b007f68 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-async/package.json @@ -0,0 +1,4 @@ +{ + "name": "lazy-loading-plugins-test", + "private": true +} diff --git a/packages/cli/snap-tests/vite-plugins-async/snap.txt b/packages/cli/snap-tests/vite-plugins-async/snap.txt new file mode 100644 index 0000000000..d37d55ef72 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-async/snap.txt @@ -0,0 +1,4 @@ +> # 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/vite-plugins-async/steps.json b/packages/cli/snap-tests/vite-plugins-async/steps.json new file mode 100644 index 0000000000..6d3c0fd185 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-async/steps.json @@ -0,0 +1,10 @@ +{ + "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'" + ] +} diff --git a/packages/cli/snap-tests/vite-plugins-async/vite.config.ts b/packages/cli/snap-tests/vite-plugins-async/vite.config.ts new file mode 100644 index 0000000000..ced83ee820 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-async/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig, vitePlugins } from 'vite-plus'; + +export default defineConfig({ + plugins: [ + vitePlugins(async () => { + const { default: myLazyPlugin } = await import('./my-plugin'); + return [myLazyPlugin()]; + }), + ], +}); 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/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..ef1451260a --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig, vitePlugins } from 'vite-plus'; + +export default defineConfig({ + plugins: [ + vitePlugins(async () => { + const { default: heavyPlugin } = await import('./heavy-plugin'); + return [heavyPlugin()]; + }), + ], +}); 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..756e9b2d24 --- /dev/null +++ b/packages/cli/snap-tests/vite-plugins-sync/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig, vitePlugins } from 'vite-plus'; + +import mySyncPlugin from './my-plugin'; + +export default defineConfig({ + plugins: [vitePlugins(() => [mySyncPlugin()])], +}); diff --git a/packages/cli/src/__tests__/index.spec.ts b/packages/cli/src/__tests__/index.spec.ts index ca5757de8b..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,124 +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('vitePlugins returns undefined when VP_COMMAND is unset', () => { + delete process.env.VP_COMMAND; + const result = vitePlugins(() => [{ name: 'test' }]); + expect(result).toBeUndefined(); }); -test('should handle lazy with empty plugins array', async () => { - const config = await defineConfig({ - lazy: () => Promise.resolve({ plugins: [] }), - }); - expect(config.plugins?.length).toBe(0); +test('vitePlugins returns undefined when VP_COMMAND is empty string', () => { + process.env.VP_COMMAND = ''; + const result = vitePlugins(() => [{ name: 'test' }]); + expect(result).toBeUndefined(); }); -test('should handle lazy returning undefined plugins', async () => { - const config = await defineConfig({ - lazy: () => Promise.resolve({}), - }); - expect(config.plugins?.length).toBe(0); -}); +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('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.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('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('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(result).toBeInstanceOf(Promise); + expect(await result).toEqual([{ name: 'async-plugin' }]); }); -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('vitePlugins returns undefined for async callback when skipped', () => { + process.env.VP_COMMAND = 'lint'; + const result = vitePlugins(async () => { + return [{ name: 'async-plugin' }]; + }); + 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..a93c478a76 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,13 @@ 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 };