vite-plugin-virtual-link
v0.0.3
Published
Vite plugin for developing against local projects on disk — with correct dependency isolation, zero symlinks, and no changes to node_modules.
Maintainers
Readme
vite-plugin-virtual-link
Vite plugin for developing against local projects on disk — with correct dependency isolation, zero symlinks, and no changes to node_modules.
Problems it solves
You're building a Vite app and want to iterate on another local package at the same time. The traditional tools (npm link, yarn link, pnpm link) cause real problems, and resolve.alias might not solve them either:
| | npm link / yarn link / pnpm link | resolve.alias | vite-plugin-virtual-link |
|---|:---:|:---:|:---:|
| HMR | ❌ doesn't reliably detect changes, preserveSymlinks: true breaks HMR entirely | ✅ | ✅ |
| Transitive dependencies | ❌ only places a symlink in your app's node_modules, which intermediate packages never look at | ✅ | ✅ |
| Aliased packages | ❌ always uses the package's own name field | ✅ | ✅ |
| No node_modules mutation | ❌ manual, stateful process that's easy to forget to clean up | ✅ | ✅ |
| CJS/ESM handling | ⚠️ depends on preserveSymlinks and optimizer config | ❌ CJS files may break without optimizer | ✅ |
| Single framework instance | ❌ React, Vue, etc. loaded twice, breaking hooks, context, and state sharing | ❌ no deduplication of shared deps | ✅ |
| Dependency isolation | ❌ linked library can import packages hoisted from the app's tree | ❌ can't isolate version-mismatched deps | ✅ |
| Scoped linking | ❌ | ❌ alias is all-or-nothing | ✅ |
[!NOTE] Why not workspaces? Workspaces (npm, yarn, or pnpm) require all packages in the same repo. This plugin works across separate repos — useful for iterating on third-party packages, forks, or packages from other teams without restructuring your project.
What it does
Given two separate projects on disk:
your-app/ (Vite root) utils/ (local, NOT linked)
├── node_modules/ ├── node_modules/
│ ├── react@18 │ ├── react@18 ← same major
│ ├── lodash@3 │ └── lodash@4 ← different major
│ └── [email protected] ├── index.js
├── vite.config.ts └── package.json
└── src/
└── app.tsxWith the plugin enabled, Vite sees this:
your-app/ (Vite root)
├── node_modules/
│ ├── react@18 ← shared (same major)
│ ├── lodash@3
│ └── [virtual] utils/ ← served from /path/to/utils
│ ├── index.js
│ └── node_modules/
│ └── lodash@4 ← isolated (different major)
├── vite.config.ts
└── src/
└── app.tsx[!TIP] Shared deps stay shared (so React hooks and context work), while version-mismatched deps stay isolated (so nothing breaks at runtime).
HMR and source maps work out of the box — edits to the local project trigger hot updates in the browser just like any other module.
Install
npm install -D vite-plugin-virtual-linkUsage
// vite.config.ts
import { defineConfig } from "vite";
import { virtualLink } from "vite-plugin-virtual-link";
export default defineConfig({
plugins: [
virtualLink({
// Redirect all imports of "utils" to your local copy
"utils": "/path/to/utils",
// Works for aliases! e.g. in package.json: `"utils-aliased": "npm:utils@^1.0.0"`
"utils-aliased": "/path/to/utils",
// Scoped: only redirect imports of "utils" under framework's subtree
// app/node_modules/framework/node_modules/utils 🔀 redirected
// app/node_modules/utils ⏹️ left alone
"utils": { dir: "/path/to/utils", scope: "framework" },
}),
],
});API
virtualLink(links, options?)
links
Record<string, string | { dir: string; scope?: string }> — map of package names to local project directories.
- String — absolute path to the local project directory. Redirects all imports of the package.
- Object:
dir— absolute path to the local project directory.scope— optional. Only redirect imports that would normally resolve under the named package'snode_modulessubtree. Withoutscope, behaves the same as the string form.
options.resolveMain
boolean — default false. Include "main" in the resolver's mainFields. Enable when dependencies only declare "main" in their package.json and resolution fails.
Limitations
[!WARNING] Dev mode only — the plugin relies on Vite's on-demand dep optimizer, which is not available during
vite build. The plugin warns and disables itself in build mode.
[!NOTE] Yarn PnP is not supported. The plugin relies on
node_modulesdirectory layout for dependency resolution and version comparison. Yarn Plug'n'Play replacesnode_moduleswith a runtime loader (.pnp.cjs) and zip archives, which is fundamentally incompatible with the current approach. npm and pnpm (including strict isolation) are fully supported.
License
MIT
