Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
],
Expand Down
2 changes: 1 addition & 1 deletion docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
42 changes: 42 additions & 0 deletions docs/config/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -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()]),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vitePlugins naming is a bit confusing to me. If we can somehow indicate that they are skipped for vp lint/fmt/check in the helper name that'd be nice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closest I come is lazyPlugins but that might be too ambiguous. Another one was viteOnlyPlugins.

],
});
```

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.
:::
8 changes: 8 additions & 0 deletions packages/cli/snap-tests/vite-plugins-async/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!doctype html>
<html>
<body>
<script type="module">
console.log('hello');
</script>
</body>
</html>
8 changes: 8 additions & 0 deletions packages/cli/snap-tests/vite-plugins-async/my-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function myLazyPlugin() {
return {
name: 'my-lazy-plugin',
transformIndexHtml(html: string) {
return html.replace('</body>', '<!-- lazy-plugin-injected --></body>');
},
};
}
4 changes: 4 additions & 0 deletions packages/cli/snap-tests/vite-plugins-async/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "lazy-loading-plugins-test",
"private": true
}
3 changes: 3 additions & 0 deletions packages/cli/snap-tests/vite-plugins-async/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> vp build
> cat dist/index.html | grep 'lazy-plugin-injected' # async vitePlugins() should apply plugins during build
<!-- lazy-plugin-injected --></body>
9 changes: 9 additions & 0 deletions packages/cli/snap-tests/vite-plugins-async/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"commands": [
{
"command": "vp build",
"ignoreOutput": true
},
"cat dist/index.html | grep 'lazy-plugin-injected' # async vitePlugins() should apply plugins during build"
]
}
10 changes: 10 additions & 0 deletions packages/cli/snap-tests/vite-plugins-async/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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()];
}),
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
throw new Error('Plugins should not be loaded during lint');

export default function heavyPlugin() {
return { name: 'heavy-plugin' };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "vite-plugins-skip-on-lint-test",
"private": true
}
3 changes: 3 additions & 0 deletions packages/cli/snap-tests/vite-plugins-skip-on-lint/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> vp lint src/ # vp lint should not load plugins (heavy-plugin.ts throws if imported)
Found 0 warnings and 0 errors.
Finished in <variable>ms on 1 file with <variable> rules using <variable> threads.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const foo = 'bar';
5 changes: 5 additions & 0 deletions packages/cli/snap-tests/vite-plugins-skip-on-lint/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"commands": [
"vp lint src/ # vp lint should not load plugins (heavy-plugin.ts throws if imported)"
]
}
10 changes: 10 additions & 0 deletions packages/cli/snap-tests/vite-plugins-skip-on-lint/vite.config.ts
Original file line number Diff line number Diff line change
@@ -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()];
}),
],
});
8 changes: 8 additions & 0 deletions packages/cli/snap-tests/vite-plugins-sync/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!doctype html>
<html>
<body>
<script type="module">
console.log('hello');
</script>
</body>
</html>
8 changes: 8 additions & 0 deletions packages/cli/snap-tests/vite-plugins-sync/my-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function mySyncPlugin() {
return {
name: 'my-sync-plugin',
transformIndexHtml(html: string) {
return html.replace('</body>', '<!-- sync-plugin-injected --></body>');
},
};
}
4 changes: 4 additions & 0 deletions packages/cli/snap-tests/vite-plugins-sync/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "vite-plugins-sync-test",
"private": true
}
3 changes: 3 additions & 0 deletions packages/cli/snap-tests/vite-plugins-sync/snap.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> vp build
> cat dist/index.html | grep 'sync-plugin-injected' # sync vitePlugins() should apply plugins during build
<!-- sync-plugin-injected --></body>
9 changes: 9 additions & 0 deletions packages/cli/snap-tests/vite-plugins-sync/steps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"commands": [
{
"command": "vp build",
"ignoreOutput": true
},
"cat dist/index.html | grep 'sync-plugin-injected' # sync vitePlugins() should apply plugins during build"
]
}
7 changes: 7 additions & 0 deletions packages/cli/snap-tests/vite-plugins-sync/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig, vitePlugins } from 'vite-plus';

import mySyncPlugin from './my-plugin';

export default defineConfig({
plugins: [vitePlugins(() => [mySyncPlugin()])],
});
168 changes: 55 additions & 113 deletions packages/cli/src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,136 +8,78 @@ 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();
expect(defaultInclude).toBeDefined();
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();
});
1 change: 1 addition & 0 deletions packages/cli/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Loading
Loading