Skip to content

Commit a21216e

Browse files
authored
feat(vite-plus): support lazy field in vite-plus config (#526)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Adds new config evaluation behavior that can change when/which plugins are included, and adjusts CLI build output (new CJS artifact) and CI build ordering; mistakes here could break consumer configs or packaging. > > **Overview** > Adds a custom `defineConfig` wrapper in `vite-plus` that supports a `lazy()` field to asynchronously provide additional plugins and merges them with any existing `plugins`, working for object, promise, and function config forms. > > Updates the CLI entrypoints and build to ship `define-config` for both ESM and CommonJS (emitting `define-config.cjs`), and adds thorough unit coverage for the new `lazy` behavior. > > Adjusts CI build steps so `vite-plus`/`vite-plus-cli` TypeScript builds run earlier, updates the repo `vite.config.ts` to use `defineConfig`, and makes small workspace/lint script tweaks. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2f8417b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 833c515 commit a21216e

9 files changed

Lines changed: 274 additions & 28 deletions

File tree

.github/actions/build-upstream/action.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ runs:
4848
pnpm --filter vite build-types
4949
pnpm --filter=@voidzero-dev/vite-plus-core build
5050
pnpm --filter=@voidzero-dev/vite-plus-test build
51+
pnpm --filter=vite-plus build-ts
52+
pnpm --filter=vite-plus-cli build-ts
5153
5254
# NAPI builds - only run on cache miss (slow, especially on Windows)
5355
# Must run before vite-plus/vite-plus-cli TypeScript builds which depend on the bindings
@@ -94,10 +96,3 @@ runs:
9496
packages/global/binding/index.js
9597
packages/global/binding/index.d.ts
9698
key: ${{ steps.cache-key.outputs.key }}
97-
98-
# Build vite-plus TypeScript after native bindings are ready
99-
- name: Build vite-plus TypeScript packages
100-
shell: bash
101-
run: |
102-
pnpm --filter=vite-plus build-ts
103-
pnpm --filter=vite-plus-cli build-ts

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"install-global-cli": "pnpm --filter=vite-plus-cli copy-binding && tool install-global-cli vite",
1111
"install-global-cli:local": "pnpm --filter=vite-plus-cli copy-binding && tool install-global-cli vp",
1212
"tsgo": "tsgo -b tsconfig.json",
13-
"lint": "vite lint --type-aware --type-check --threads 4",
13+
"lint": "vite lint --type-aware --threads 4",
1414
"test": "vite test run && pnpm -r snap-test",
1515
"fmt": "vite fmt",
1616
"test:unit": "vite test run",

packages/cli/build.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
*/
1717

1818
import { existsSync, globSync, readdirSync, statSync } from 'node:fs';
19-
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
19+
import { copyFile, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2020
import { dirname, join } from 'node:path';
2121
import { fileURLToPath } from 'node:url';
22+
import { parseArgs } from 'node:util';
2223

2324
import { createBuildCommand, NapiCli } from '@napi-rs/cli';
2425
import { format } from 'oxfmt';
@@ -29,25 +30,36 @@ import {
2930
parseJsonSourceFileConfigFileContent,
3031
readJsonConfigFile,
3132
sys,
33+
ModuleKind,
3234
} from 'typescript';
3335

3436
const projectDir = dirname(fileURLToPath(import.meta.url));
3537
const TEST_PACKAGE_NAME = '@voidzero-dev/vite-plus-test';
3638
const CORE_PACKAGE_NAME = '@voidzero-dev/vite-plus-core';
3739

38-
const skipNative = process.argv.includes('--skip-native');
39-
const skipTs = process.argv.includes('--skip-ts');
40+
const {
41+
values: { ['skip-native']: skipNative, ['skip-ts']: skipTs },
42+
} = parseArgs({
43+
options: {
44+
['skip-native']: { type: 'boolean', default: false },
45+
['skip-ts']: { type: 'boolean', default: false },
46+
},
47+
strict: false,
48+
});
49+
4050
// Filter out custom flags before passing to NAPI CLI
4151
const napiArgs = process.argv
4252
.slice(2)
4353
.filter((arg) => arg !== '--skip-native' && arg !== '--skip-ts');
4454

55+
if (!skipTs) {
56+
await buildCli();
57+
}
4558
// Build native first - TypeScript may depend on the generated binding types
4659
if (!skipNative) {
4760
await buildNapiBinding();
4861
}
4962
if (!skipTs) {
50-
await buildCli();
5163
await syncCorePackageExports();
5264
await syncTestPackageExports();
5365
}
@@ -106,11 +118,38 @@ async function buildCli() {
106118
sys,
107119
projectDir,
108120
);
121+
109122
const options = {
110123
...initialOptions,
111124
noEmit: false,
112125
outDir: join(projectDir, 'dist'),
113126
};
127+
128+
const cjsHost = createCompilerHost({
129+
...options,
130+
module: ModuleKind.CommonJS,
131+
});
132+
133+
const cjsProgram = createProgram({
134+
rootNames: ['src/define-config.ts'],
135+
options: {
136+
...options,
137+
module: ModuleKind.CommonJS,
138+
},
139+
host: cjsHost,
140+
});
141+
142+
const { diagnostics: cjsDiagnostics } = cjsProgram.emit();
143+
144+
if (cjsDiagnostics.length > 0) {
145+
console.error(formatDiagnostics(cjsDiagnostics, cjsHost));
146+
process.exit(1);
147+
}
148+
await rename(
149+
join(projectDir, 'dist/define-config.js'),
150+
join(projectDir, 'dist/define-config.cjs'),
151+
);
152+
114153
const host = createCompilerHost(options);
115154

116155
const program = createProgram({

packages/cli/src/__tests__/index.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,125 @@ test('should keep vitest exports stable', () => {
1919
expect(defaultInclude).toBeDefined();
2020
expect(defaultBrowserPort).toBeDefined();
2121
});
22+
23+
test('should support lazy loading of plugins', async () => {
24+
const config = await defineConfig({
25+
lazy: () => Promise.resolve({ plugins: [{ name: 'test' }] }),
26+
});
27+
expect(config.plugins?.length).toBe(1);
28+
});
29+
30+
test('should merge lazy plugins with existing plugins', async () => {
31+
const config = await defineConfig({
32+
plugins: [{ name: 'existing' }],
33+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
34+
});
35+
expect(config.plugins?.length).toBe(2);
36+
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
37+
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
38+
});
39+
40+
test('should handle lazy with empty plugins array', async () => {
41+
const config = await defineConfig({
42+
lazy: () => Promise.resolve({ plugins: [] }),
43+
});
44+
expect(config.plugins?.length).toBe(0);
45+
});
46+
47+
test('should handle lazy returning undefined plugins', async () => {
48+
const config = await defineConfig({
49+
lazy: () => Promise.resolve({}),
50+
});
51+
expect(config.plugins?.length).toBe(0);
52+
});
53+
54+
test('should handle Promise config with lazy', async () => {
55+
const config = await defineConfig(
56+
Promise.resolve({
57+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-promise' }] }),
58+
}),
59+
);
60+
expect(config.plugins?.length).toBe(1);
61+
expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-promise');
62+
});
63+
64+
test('should handle Promise config with lazy and existing plugins', async () => {
65+
const config = await defineConfig(
66+
Promise.resolve({
67+
plugins: [{ name: 'existing' }],
68+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
69+
}),
70+
);
71+
expect(config.plugins?.length).toBe(2);
72+
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
73+
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
74+
});
75+
76+
test('should handle Promise config without lazy', async () => {
77+
const config = await defineConfig(
78+
Promise.resolve({
79+
plugins: [{ name: 'no-lazy' }],
80+
}),
81+
);
82+
expect(config.plugins?.length).toBe(1);
83+
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
84+
});
85+
86+
test('should handle function config with lazy', async () => {
87+
const configFn = defineConfig(() => ({
88+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-fn' }] }),
89+
}));
90+
expect(typeof configFn).toBe('function');
91+
const config = await configFn({ command: 'build', mode: 'production' });
92+
expect(config.plugins?.length).toBe(1);
93+
expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-fn');
94+
});
95+
96+
test('should handle function config with lazy and existing plugins', async () => {
97+
const configFn = defineConfig(() => ({
98+
plugins: [{ name: 'existing' }],
99+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
100+
}));
101+
const config = await configFn({ command: 'build', mode: 'production' });
102+
expect(config.plugins?.length).toBe(2);
103+
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
104+
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
105+
});
106+
107+
test('should handle function config without lazy', () => {
108+
const configFn = defineConfig(() => ({
109+
plugins: [{ name: 'no-lazy' }],
110+
}));
111+
const config = configFn({ command: 'build', mode: 'production' });
112+
expect(config.plugins?.length).toBe(1);
113+
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
114+
});
115+
116+
test('should handle async function config with lazy', async () => {
117+
const configFn = defineConfig(async () => ({
118+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy-from-async-fn' }] }),
119+
}));
120+
const config = await configFn({ command: 'build', mode: 'production' });
121+
expect(config.plugins?.length).toBe(1);
122+
expect((config.plugins?.[0] as { name: string })?.name).toBe('lazy-from-async-fn');
123+
});
124+
125+
test('should handle async function config with lazy and existing plugins', async () => {
126+
const configFn = defineConfig(async () => ({
127+
plugins: [{ name: 'existing' }],
128+
lazy: () => Promise.resolve({ plugins: [{ name: 'lazy' }] }),
129+
}));
130+
const config = await configFn({ command: 'build', mode: 'production' });
131+
expect(config.plugins?.length).toBe(2);
132+
expect((config.plugins?.[0] as { name: string })?.name).toBe('existing');
133+
expect((config.plugins?.[1] as { name: string })?.name).toBe('lazy');
134+
});
135+
136+
test('should handle async function config without lazy', async () => {
137+
const configFn = defineConfig(async () => ({
138+
plugins: [{ name: 'no-lazy' }],
139+
}));
140+
const config = await configFn({ command: 'build', mode: 'production' });
141+
expect(config.plugins?.length).toBe(1);
142+
expect((config.plugins?.[0] as { name: string })?.name).toBe('no-lazy');
143+
});

packages/cli/src/define-config.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
defineConfig as viteDefineConfig,
3+
type ConfigEnv,
4+
} from '@voidzero-dev/vite-plus-test/config';
5+
6+
import type { UserConfig } from './index';
7+
8+
type ViteUserConfigFnObject = (env: ConfigEnv) => UserConfig;
9+
type ViteUserConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig>;
10+
type ViteUserConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>;
11+
type ViteUserConfigExport =
12+
| UserConfig
13+
| Promise<UserConfig>
14+
| ViteUserConfigFnObject
15+
| ViteUserConfigFnPromise
16+
| ViteUserConfigFn;
17+
18+
export function defineConfig(config: UserConfig): UserConfig;
19+
export function defineConfig(config: Promise<UserConfig>): Promise<UserConfig>;
20+
export function defineConfig(config: ViteUserConfigFnObject): ViteUserConfigFnObject;
21+
export function defineConfig(config: ViteUserConfigFnPromise): ViteUserConfigFnPromise;
22+
export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport;
23+
24+
export function defineConfig(config: ViteUserConfigExport): ViteUserConfigExport {
25+
if (typeof config === 'object') {
26+
if (config instanceof Promise) {
27+
return config.then((config) => {
28+
if (config.lazy) {
29+
return config.lazy().then(({ plugins }) =>
30+
viteDefineConfig({
31+
...config,
32+
plugins: [...(config.plugins || []), ...(plugins || [])],
33+
}),
34+
);
35+
}
36+
return viteDefineConfig(config);
37+
});
38+
} else if (config.lazy) {
39+
return config.lazy().then(({ plugins }) =>
40+
viteDefineConfig({
41+
...config,
42+
plugins: [...(config.plugins || []), ...(plugins || [])],
43+
}),
44+
);
45+
}
46+
} else if (typeof config === 'function') {
47+
return viteDefineConfig((env) => {
48+
const c = config(env);
49+
if (c instanceof Promise) {
50+
return c.then((v) => {
51+
if (v.lazy) {
52+
return v
53+
.lazy()
54+
.then(({ plugins }) =>
55+
viteDefineConfig({ ...v, plugins: [...(v.plugins || []), ...(plugins || [])] }),
56+
);
57+
}
58+
return v;
59+
});
60+
}
61+
if (c.lazy) {
62+
return c
63+
.lazy()
64+
.then(({ plugins }) => ({ ...c, plugins: [...(c.plugins || []), ...(plugins || [])] }));
65+
}
66+
return c;
67+
});
68+
}
69+
return viteDefineConfig(config);
70+
}

packages/cli/src/index.cts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ const vite = require('@voidzero-dev/vite-plus-core');
22

33
const vitest = require('@voidzero-dev/vite-plus-test/config');
44

5+
const { defineConfig } = require('./define-config');
6+
57
module.exports = {
68
...vite,
79
...vitest,
10+
defineConfig,
811
};

packages/cli/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { defineConfig } from '@voidzero-dev/vite-plus-test/config';
1+
import { type Plugin as VitestPlugin } from '@voidzero-dev/vite-plus-test/config';
22

3+
import { defineConfig } from './define-config.js';
34
import type { LibUserConfig } from './lib';
45
import type { FormatOptions } from './oxfmt-config';
56
import type { OxlintConfig } from './oxlint-config';
@@ -17,6 +18,12 @@ declare module '@voidzero-dev/vite-plus-core' {
1718
lib?: LibUserConfig | LibUserConfig[];
1819

1920
tasks?: Tasks;
21+
22+
// temporary solution to load plugins lazily
23+
// We need to support this in the upstream vite
24+
lazy?: () => Promise<{
25+
plugins?: VitestPlugin[];
26+
}>;
2027
}
2128
}
2229

0 commit comments

Comments
 (0)