Skip to content
Closed
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
71 changes: 63 additions & 8 deletions packages/cli/src/__tests__/define-config-plugins.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

Expand Down Expand Up @@ -29,6 +29,7 @@ vi.mock('@voidzero-dev/vite-task-client', () => taskClientMocks);
const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver';
const COVERAGE_GUARD_PLUGIN_NAME = 'vite-plus:coverage-version-guard';
const CACHE_PATHS_PLUGIN_NAME = 'vite-plus:vitest-cache-paths';
const MODULES_YAML_PLUGIN_NAME = 'vite-plus:pnpm-modules-yaml';

function pluginName(p: unknown): string | undefined {
if (p && typeof p === 'object' && 'name' in p && typeof p.name === 'string') {
Expand All @@ -46,7 +47,8 @@ describe('defineConfig project plugin injection', () => {
expect(pluginName(result.plugins[1])).toBe(AUTO_INLINE_PLUGIN_NAME);
expect(pluginName(result.plugins[2])).toBe(COVERAGE_GUARD_PLUGIN_NAME);
expect(pluginName(result.plugins[3])).toBe(CACHE_PATHS_PLUGIN_NAME);
expect(pluginName(result.plugins[4])).toBe('user-existing-root-plugin');
expect(pluginName(result.plugins[4])).toBe(MODULES_YAML_PLUGIN_NAME);
expect(pluginName(result.plugins[5])).toBe('user-existing-root-plugin');
});

it('injects resolver + auto-inline plugins into an inline-object project entry, preserving existing plugins', () => {
Expand All @@ -69,9 +71,10 @@ describe('defineConfig project plugin injection', () => {
expect(pluginName(project.plugins[1])).toBe(AUTO_INLINE_PLUGIN_NAME);
expect(pluginName(project.plugins[2])).toBe(COVERAGE_GUARD_PLUGIN_NAME);
expect(pluginName(project.plugins[3])).toBe(CACHE_PATHS_PLUGIN_NAME);
expect(pluginName(project.plugins[4])).toBe('user-unit-project-plugin');
expect(pluginName(project.plugins[4])).toBe(MODULES_YAML_PLUGIN_NAME);
expect(pluginName(project.plugins[5])).toBe('user-unit-project-plugin');
// Sanity: the existing plugin reference is preserved (clone shallow-copies the array).
expect(project.plugins[4]).toBe(existing);
expect(project.plugins[5]).toBe(existing);
});

it('injects plugins into the return value of a function-shaped project entry', () => {
Expand All @@ -96,7 +99,8 @@ describe('defineConfig project plugin injection', () => {
expect(pluginName(resolved.plugins[1])).toBe(AUTO_INLINE_PLUGIN_NAME);
expect(pluginName(resolved.plugins[2])).toBe(COVERAGE_GUARD_PLUGIN_NAME);
expect(pluginName(resolved.plugins[3])).toBe(CACHE_PATHS_PLUGIN_NAME);
expect(pluginName(resolved.plugins[4])).toBe('user-fn-project-plugin');
expect(pluginName(resolved.plugins[4])).toBe(MODULES_YAML_PLUGIN_NAME);
expect(pluginName(resolved.plugins[5])).toBe('user-fn-project-plugin');
});

it('passes string-glob project entries through unchanged', () => {
Expand All @@ -123,11 +127,12 @@ describe('defineConfig project plugin injection', () => {

const project = result.test.projects[0] as { plugins: unknown[]; test: { name: string } };
expect(project.test.name).toBe('no-plugins');
expect(project.plugins).toHaveLength(4);
expect(project.plugins).toHaveLength(5);
expect(pluginName(project.plugins[0])).toBe(RESOLVER_PLUGIN_NAME);
expect(pluginName(project.plugins[1])).toBe(AUTO_INLINE_PLUGIN_NAME);
expect(pluginName(project.plugins[2])).toBe(COVERAGE_GUARD_PLUGIN_NAME);
expect(pluginName(project.plugins[3])).toBe(CACHE_PATHS_PLUGIN_NAME);
expect(pluginName(project.plugins[4])).toBe(MODULES_YAML_PLUGIN_NAME);
});

it.each([undefined, true])('inherits root plugins with extends %s', (extendsRoot) => {
Expand Down Expand Up @@ -160,6 +165,7 @@ describe('defineConfig project plugin injection', () => {
AUTO_INLINE_PLUGIN_NAME,
COVERAGE_GUARD_PLUGIN_NAME,
CACHE_PATHS_PLUGIN_NAME,
MODULES_YAML_PLUGIN_NAME,
]);
});

Expand All @@ -170,6 +176,7 @@ describe('defineConfig project plugin injection', () => {
AUTO_INLINE_PLUGIN_NAME,
COVERAGE_GUARD_PLUGIN_NAME,
CACHE_PATHS_PLUGIN_NAME,
MODULES_YAML_PLUGIN_NAME,
'user',
]);
});
Expand All @@ -180,9 +187,9 @@ describe('defineConfig project plugin injection', () => {
projects: [{ extends: false, test: { name: 'child' } }, { test: { name: 'inherited' } }],
},
});
expect(config.plugins).toHaveLength(4);
expect(config.plugins).toHaveLength(5);
const [independent, inherited] = config.test!.projects! as { plugins?: unknown[] }[];
expect(independent.plugins).toHaveLength(4);
expect(independent.plugins).toHaveLength(5);
expect(inherited.plugins).toBeUndefined();
});
});
Expand Down Expand Up @@ -583,6 +590,54 @@ describe('defineConfig vitest cache paths plugin', () => {
});
});

function runConfigHook(root: string | undefined) {
const result = defineConfig({ root }) as { plugins: unknown[] };
const plugin = result.plugins.find(
(p): p is { config: (config: { root?: string }) => void } =>
pluginName(p) === MODULES_YAML_PLUGIN_NAME,
);
if (!plugin) {
throw new Error('pnpm modules.yaml plugin not found');
}
plugin.config({ root });
}

describe('defineConfig pnpm modules.yaml plugin', () => {
afterEach(() => {
taskClientMocks.ignoreInput.mockClear();
});

it('ignores the workspace node_modules/.modules.yaml before Vite reads it', () => {
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'vp-modules-yaml-')));
writeFileSync(join(root, 'package.json'), '{"name":"probe","private":true}\n');
try {
runConfigHook(root);
expect(taskClientMocks.ignoreInput).toHaveBeenCalledTimes(1);
expect(taskClientMocks.ignoreInput).toHaveBeenCalledWith(
join(root, 'node_modules', '.modules.yaml'),
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it('anchors at the workspace root when the Vite root is a package inside it', () => {
const root = realpathSync.native(mkdtempSync(join(tmpdir(), 'vp-modules-yaml-ws-')));
writeFileSync(join(root, 'package.json'), '{"name":"ws","private":true}\n');
writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - pkgs/*\n');
mkdirSync(join(root, 'pkgs', 'app'), { recursive: true });
writeFileSync(join(root, 'pkgs', 'app', 'package.json'), '{"name":"app"}\n');
try {
runConfigHook(join(root, 'pkgs', 'app'));
expect(taskClientMocks.ignoreInput).toHaveBeenCalledWith(
join(root, 'node_modules', '.modules.yaml'),
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});

/** Extract the coverage-version-guard plugin from a fresh defineConfig. */
function getCoverageGuard(): { configureVitest: (ctx: unknown) => void } {
const result = defineConfig({}) as { plugins: unknown[] };
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,16 @@ const RESOLVER_PLUGIN_NAME = 'vite-plus:vitest-resolver';
const AUTO_INLINE_PLUGIN_NAME = 'vite-plus:auto-inline-matcher-deps';
const COVERAGE_GUARD_PLUGIN_NAME = 'vite-plus:coverage-version-guard';
const CACHE_PATHS_PLUGIN_NAME = 'vite-plus:vitest-cache-paths';
const MODULES_YAML_PLUGIN_NAME = 'vite-plus:pnpm-modules-yaml';
const userPlugins = (plugins: unknown): unknown[] => {
expect(Array.isArray(plugins)).toBe(true);
const arr = plugins as unknown[];
expect((arr[0] as { name?: string })?.name).toBe(RESOLVER_PLUGIN_NAME);
expect((arr[1] as { name?: string })?.name).toBe(AUTO_INLINE_PLUGIN_NAME);
expect((arr[2] as { name?: string })?.name).toBe(COVERAGE_GUARD_PLUGIN_NAME);
expect((arr[3] as { name?: string })?.name).toBe(CACHE_PATHS_PLUGIN_NAME);
return arr.slice(4);
expect((arr[4] as { name?: string })?.name).toBe(MODULES_YAML_PLUGIN_NAME);
return arr.slice(5);
};

// lazyPlugins type compatibility tests — these verify at compile time that
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/src/define-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,24 @@ function vitePlusVitestCachePathsPlugin(): PluginOption {
} as PluginOption;
}

/**
* Vite reads `<workspaceRoot>/node_modules/.modules.yaml` while resolving the
* server options, to find the pnpm virtual store. pnpm rewrites that file on
* every install (`prunedAt` changes), so under `vp run` the tracked read makes
* every task miss its cache after a fresh install. Tell the runner the file is
* not an input. The `config` hook runs before Vite resolves the server
* options. The call is a no-op outside `vp run`.
*/
function vitePlusPnpmModulesYamlPlugin(): PluginOption {
return {
name: 'vite-plus:pnpm-modules-yaml',
config(config) {
const root = resolve(config.root ?? process.cwd());
ignoreInput(join(searchForWorkspaceRoot(root), 'node_modules', '.modules.yaml'));
},
};
}

/**
* Inject the vitest resolver plugin, the auto-inline matcher plugin, the
* coverage version guard, and the cache-paths plugin into a single inline
Expand All @@ -845,6 +863,7 @@ const VITE_PLUS_TEST_PLUGIN_NAMES: ReadonlySet<string> = new Set([
'vite-plus:auto-inline-matcher-deps',
'vite-plus:coverage-version-guard',
'vite-plus:vitest-cache-paths',
'vite-plus:pnpm-modules-yaml',
]);

function withoutVitePlusTestPlugins(plugin: PluginOption): PluginOption {
Expand Down Expand Up @@ -874,6 +893,7 @@ function injectPluginIntoInlineConfig<
vitePlusAutoInlineMatcherPlugin(),
vitePlusCoverageVersionGuardPlugin(),
vitePlusVitestCachePathsPlugin(),
vitePlusPnpmModulesYamlPlugin(),
...(config.plugins ?? []).filter(keepUserPlugin).map(withoutVitePlusTestPlugins),
],
};
Expand Down