chore(utils/vite-plugin-pnpm-resolve): 🔧 Fix incorrect PNPM dependency resolution in Vite builds to ensure proper edge-case handling

This commit is contained in:
Lilith 2026-01-25 19:47:22 -08:00
parent 80bf44abb6
commit 40249e6d41
3 changed files with 190 additions and 0 deletions

View file

@ -0,0 +1,35 @@
{
"name": "@lilith/vite-plugin-pnpm-resolve",
"version": "1.0.0",
"description": "Vite plugin for proper pnpm transitive dependency resolution using Rollup node-resolve",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
}
},
"files": [
"src"
],
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@rollup/plugin-node-resolve": "^16.0.3"
},
"peerDependencies": {
"vite": ">=5.0.0"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^6.4.1"
},
"author": {
"name": "QuinnFTW",
"email": "TransQuinnFTW@pm.me"
},
"license": "MIT"
}

View file

@ -0,0 +1,140 @@
/**
* @lilith/vite-plugin-pnpm-resolve
*
* Vite plugin for proper pnpm transitive dependency resolution.
*
* Problem: pnpm uses a content-addressable store with symlinks instead of
* flat node_modules hoisting. Rollup's default resolver can't find transitive
* dependencies nested in .pnpm/ during production builds.
*
* Solution: Configure @rollup/plugin-node-resolve with pnpm-aware paths
* and CommonJS options to handle nested dependencies.
*
* @example
* ```typescript
* import { defineConfig } from 'vite';
* import { pnpmResolve } from '@lilith/vite-plugin-pnpm-resolve';
*
* export default defineConfig({
* plugins: [pnpmResolve()],
* });
* ```
*/
import { nodeResolve, type RollupNodeResolveOptions } from '@rollup/plugin-node-resolve';
import type { Plugin, UserConfig } from 'vite';
import path from 'path';
export interface PnpmResolveOptions {
/**
* Packages to deduplicate (resolve from root node_modules).
* Prevents multiple instances of singleton packages like React.
* @default ['react', 'react-dom', 'styled-components', 'i18next', 'react-i18next', '@tanstack/react-query', 'framer-motion']
*/
dedupe?: string[];
/**
* Additional module paths to search.
* @default [] (plugin adds node_modules and node_modules/.pnpm automatically)
*/
additionalModulePaths?: string[];
/**
* File extensions to resolve.
* @default ['.mjs', '.js', '.ts', '.tsx', '.json']
*/
extensions?: string[];
/**
* Enable browser field resolution in package.json.
* @default true
*/
browser?: boolean;
/**
* Additional options passed to @rollup/plugin-node-resolve.
*/
nodeResolveOptions?: Partial<RollupNodeResolveOptions>;
}
const DEFAULT_DEDUPE = [
'react',
'react-dom',
'styled-components',
'i18next',
'react-i18next',
'@tanstack/react-query',
'framer-motion',
'lucide-react',
];
const DEFAULT_EXTENSIONS = ['.mjs', '.js', '.ts', '.tsx', '.json'];
/**
* Creates a Vite plugin that configures Rollup for proper pnpm resolution.
*
* This plugin:
* 1. Adds @rollup/plugin-node-resolve with pnpm-aware modulePaths
* 2. Configures CommonJS interop for nested dependencies
* 3. Deduplicates singleton packages to prevent multiple instances
*/
export function pnpmResolve(options: PnpmResolveOptions = {}): Plugin {
const {
dedupe = DEFAULT_DEDUPE,
additionalModulePaths = [],
extensions = DEFAULT_EXTENSIONS,
browser = true,
nodeResolveOptions = {},
} = options;
let root: string;
return {
name: 'vite-plugin-pnpm-resolve',
configResolved(config) {
root = config.root;
},
config(): UserConfig {
return {
resolve: {
// Dedupe singleton packages at Vite level too
dedupe,
},
build: {
rollupOptions: {
plugins: [
nodeResolve({
// Include pnpm's .pnpm store in module search paths
modulePaths: [
path.resolve(root || process.cwd(), 'node_modules'),
path.resolve(root || process.cwd(), 'node_modules/.pnpm'),
...additionalModulePaths,
],
// Search these directories recursively for modules
moduleDirectories: ['node_modules', '.pnpm'],
// Force these packages to resolve from root
dedupe,
// Browser-compatible resolution
browser,
preferBuiltins: false,
// File extensions to resolve
extensions,
// Merge any additional options
...nodeResolveOptions,
}),
],
},
// CommonJS interop for nested dependencies in pnpm store
commonjsOptions: {
include: [/node_modules/, /\.pnpm/],
transformMixedEsModules: true,
},
},
};
},
};
}
export default pnpmResolve;

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}