Describe the bug
In Vite 8, a bare import that ends in .svelte and resolves into node_modules (for example import Banner from 'svelte-lib/Banner.svelte') is not recorded as an optimizable dependency. The scanner crawls it as source instead. When that file has a relative import (import Icon from './Icon.svelte'), the import is resolved against the virtual-module: importer and fails. The whole scan is then skipped:
(!) Failed to run dependency scan. Skipping dependency pre-bundling. Error: Failed to scan for dependencies from entries:
<root>/index.html
Build failed with 1 error:
[UNRESOLVED_IMPORT] Could not resolve './Icon.svelte' in virtual-module:<root>/node_modules/svelte-lib/Banner.svelte?id=0
The visible consequence is that dependencies are only discovered at first page load. The optimizer re-runs mid-load (optimized dependencies changed. reloading), and requests for the stale /node_modules/.vite/deps/* URLs get 504. Depending on timing, the first load can fail with "Importing a module script failed" and leave a blank page until a manual reload.
Importing the same component through the package's barrel (import { Banner } from 'svelte-lib') scans cleanly, and svelte-lib is pre-bundled at startup.
Cause. In vite:dep-scan:resolve, the html-types branch intends to hand optimizable node_modules files to the bare-import branch below it:
|
if (htmlTypesRE.test(id)) { |
|
const resolved = await resolve(id, importer) |
|
if (!resolved) return |
|
// It is possible for the scanner to scan html types in node_modules. |
|
// If we can optimize this html type, skip it so it's handled by the |
|
// bare import resolve, and recorded as optimization dep. |
|
if ( |
|
isInNodeModules(resolved) && |
|
isOptimizable(resolved, optimizeDepsOptions) |
|
) |
|
return |
|
if (shouldExternalizeDep(resolved, id)) { |
|
return externalUnlessEntry({ path: id }) |
|
} |
|
return resolved |
if (htmlTypesRE.test(id)) {
const resolved = await resolve(id, importer)
if (!resolved) return
// It is possible for the scanner to scan html types in node_modules.
// If we can optimize this html type, skip it so it's handled by the
// bare import resolve, and recorded as optimization dep.
if (
isInNodeModules(resolved) &&
isOptimizable(resolved, optimizeDepsOptions)
)
return
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id })
}
return resolved
}
With esbuild these were separate onResolve callbacks, so returning undefined passed control to the next callback (the bare-import onResolve), which recorded the dep and externalized it. In the rolldown version both branches live in the same resolveId function. The return exits that function, so the bare-import branch (// bare imports: record and externalize, L624 onward) never runs for this id. Rolldown's own resolution then picks the file up, vite:dep-scan:load:html extracts its <script> into virtual-module:…?id=0, and relative imports inside it fail to resolve.
The trigger is any framework whose plugin adds its extension to optimizeDeps.extensions (@sveltejs/vite-plugin-svelte 7 always adds .svelte, so isOptimizable is true), combined with a deep .svelte import from a package installed in node_modules.
Experiment that fixes it. Make the optimizable case fall through instead of returning. Only lines 618-621 change, and they are wrapped:
if (!(isInNodeModules(resolved) && isOptimizable(resolved, optimizeDepsOptions))) {
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id })
}
return resolved
}
// otherwise fall through to the bare-import branch below
I applied the equivalent edit to node_modules/vite/dist/node/chunks/node.js in the reproduction below (vite 8.3.3). With it, the scan succeeds and _metadata.json lists svelte-lib/Banner.svelte as an optimized dependency.
Reproduction
Minimal project, described inline. There are seven files plus a packed library tarball.
package.json
{
"name": "vite-svelte-deep-import-scan-repro",
"private": true,
"type": "module",
"scripts": { "dev": "vite --force" },
"dependencies": { "svelte-lib": "file:./svelte-lib-1.0.0.tgz" },
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^7.3.0",
"svelte": "^5.56.9",
"vite": "8.3.3"
}
}
vite.config.js
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
export default defineConfig({ plugins: [svelte()] })
index.html
<!doctype html>
<html>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/main.js
import { mount } from 'svelte'
import App from './App.svelte'
mount(App, { target: document.getElementById('app') })
src/App.svelte
<script>
// Deep import of a .svelte file from a dependency: the dependency scan fails.
import Banner from 'svelte-lib/Banner.svelte'
// Barrel import of the same component: the dependency scan succeeds.
// import { Banner } from 'svelte-lib'
</script>
<Banner />
The library lives in lib/ and is packed with npm pack into svelte-lib-1.0.0.tgz. A tarball is used so the package is a real copy under node_modules. A file:./lib folder dependency is symlinked, resolves outside node_modules, and does not take this code path.
lib/package.json
{
"name": "svelte-lib",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./index.js",
"./*": "./*"
},
"peerDependencies": { "svelte": "^5.0.0" }
}
lib/index.js
export { default as Banner } from './Banner.svelte'
export { default as Icon } from './Icon.svelte'
lib/Banner.svelte
<script>
import Icon from './Icon.svelte'
</script>
<p><Icon /> hello</p>
lib/Icon.svelte
Steps to reproduce
- Create the files above.
(cd lib && npm pack --pack-destination ..)
npm install
npx vite --force. The output shows Failed to run dependency scan with [UNRESOLVED_IMPORT] Could not resolve './Icon.svelte' in virtual-module:…/node_modules/svelte-lib/Banner.svelte?id=0.
- In
src/App.svelte, switch to the barrel import (import { Banner } from 'svelte-lib') and run npx vite --force again. The scan succeeds and node_modules/.vite/deps/_metadata.json lists svelte-lib under optimized.
This minimal project reproduces it on vite 8.3.3. The same error appears with vite 8.2.1 in a larger project, and the code is unchanged on main at the commit linked above.
System Info
System:
OS: macOS 27.0.1
CPU: (16) arm64 Apple M4 Max
Memory: 2.01 GB / 48.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 26.9.0 - /opt/homebrew/bin/node
npm: 11.19.1 - /opt/homebrew/bin/npm
pnpm: 12.4.2 - /opt/homebrew/bin/pnpm
Deno: 2.9.7 - /opt/homebrew/bin/deno
Browsers:
Brave Browser: 143.1.85.111
Chrome: 154.0.8037.95
Safari: 27.0.1
npmPackages:
@sveltejs/vite-plugin-svelte: ^7.3.0 => 7.3.1
svelte: ^5.56.9 => 5.57.2
vite: 8.3.3 => 8.3.3
rolldown is not a direct dependency, so envinfo does not list it. npm ls rolldown reports rolldown@1.2.12.
Used Package Manager
npm
Logs
Click to expand!
Deep import (import Banner from 'svelte-lib/Banner.svelte'), vite 8.3.3:
[vite-plugin-svelte] no Svelte config found at <root> - using default configuration.
[vite] (client) Forced re-optimization of dependencies
[vite] (ssr) Forced re-optimization of dependencies
(!) Failed to run dependency scan. Skipping dependency pre-bundling. Error: Failed to scan for dependencies from entries:
<root>/index.html
Build failed with 1 error:
[UNRESOLVED_IMPORT] Could not resolve './Icon.svelte' in virtual-module:<root>/node_modules/svelte-lib/Banner.svelte?id=0
╭─[ virtual-module:<root>/node_modules/svelte-lib/Banner.svelte?id=0:2:20 ]
│
2 │ import Icon from './Icon.svelte'
│ ───────┬───────
│ ╰───────── Module not found.
│
│ Help: 'virtual-module:<root>/node_modules/svelte-lib/Banner.svelte?id=0' is imported by the following path:
│ - virtual-module:<root>/node_modules/svelte-lib/Banner.svelte?id=0
│ - node_modules/svelte-lib/Banner.svelte
│ - virtual-module:<root>/src/App.svelte?id=0
│ - src/App.svelte
│ - src/main.js
│ - index.html
───╯
at aggregateBindingErrorsIntoJsError (<root>/node_modules/rolldown/dist/shared/error-Bj1xBdEY.mjs:49:18)
at unwrapBindingResult (<root>/node_modules/rolldown/dist/shared/error-Bj1xBdEY.mjs:19:128)
at scan (<root>/node_modules/rolldown/dist/experimental-index.mjs:241:3)
at async Object.build (<root>/node_modules/vite/dist/node/chunks/node.js:28068:3)
at async scan (<root>/node_modules/vite/dist/node/chunks/node.js:28007:4)
at async <root>/node_modules/vite/dist/node/chunks/node.js:35783:15
VITE v8.3.3 ready in 605 ms
Barrel import (import { Banner } from 'svelte-lib'), vite 8.3.3. The scan succeeds, and _metadata.json optimized includes svelte-lib:
[vite] (client) Forced re-optimization of dependencies
[vite] (ssr) Forced re-optimization of dependencies
VITE v8.3.3 ready in 310 ms
Deep import with the fall-through experiment applied to vite 8.3.3. The scan succeeds, and _metadata.json optimized includes svelte-lib/Banner.svelte:
[vite] (client) Forced re-optimization of dependencies
[vite] (ssr) Forced re-optimization of dependencies
VITE v8.3.3 ready in 296 ms
Validations
Describe the bug
In Vite 8, a bare import that ends in
.svelteand resolves intonode_modules(for exampleimport Banner from 'svelte-lib/Banner.svelte') is not recorded as an optimizable dependency. The scanner crawls it as source instead. When that file has a relative import (import Icon from './Icon.svelte'), the import is resolved against thevirtual-module:importer and fails. The whole scan is then skipped:The visible consequence is that dependencies are only discovered at first page load. The optimizer re-runs mid-load (
optimized dependencies changed. reloading), and requests for the stale/node_modules/.vite/deps/*URLs get504. Depending on timing, the first load can fail with "Importing a module script failed" and leave a blank page until a manual reload.Importing the same component through the package's barrel (
import { Banner } from 'svelte-lib') scans cleanly, andsvelte-libis pre-bundled at startup.Cause. In
vite:dep-scan:resolve, the html-types branch intends to hand optimizable node_modules files to the bare-import branch below it:vite/packages/vite/src/node/optimizer/scan.ts
Lines 607 to 621 in fea5b21
With esbuild these were separate
onResolvecallbacks, so returningundefinedpassed control to the next callback (the bare-importonResolve), which recorded the dep and externalized it. In the rolldown version both branches live in the sameresolveIdfunction. Thereturnexits that function, so the bare-import branch (// bare imports: record and externalize, L624 onward) never runs for this id. Rolldown's own resolution then picks the file up,vite:dep-scan:load:htmlextracts its<script>intovirtual-module:…?id=0, and relative imports inside it fail to resolve.The trigger is any framework whose plugin adds its extension to
optimizeDeps.extensions(@sveltejs/vite-plugin-svelte7 always adds.svelte, soisOptimizableis true), combined with a deep.svelteimport from a package installed innode_modules.Experiment that fixes it. Make the optimizable case fall through instead of returning. Only lines 618-621 change, and they are wrapped:
I applied the equivalent edit to
node_modules/vite/dist/node/chunks/node.jsin the reproduction below (vite 8.3.3). With it, the scan succeeds and_metadata.jsonlistssvelte-lib/Banner.svelteas an optimized dependency.Reproduction
Minimal project, described inline. There are seven files plus a packed library tarball.
package.json{ "name": "vite-svelte-deep-import-scan-repro", "private": true, "type": "module", "scripts": { "dev": "vite --force" }, "dependencies": { "svelte-lib": "file:./svelte-lib-1.0.0.tgz" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^7.3.0", "svelte": "^5.56.9", "vite": "8.3.3" } }vite.config.jsindex.htmlsrc/main.jssrc/App.svelteThe library lives in
lib/and is packed withnpm packintosvelte-lib-1.0.0.tgz. A tarball is used so the package is a real copy undernode_modules. Afile:./libfolder dependency is symlinked, resolves outsidenode_modules, and does not take this code path.lib/package.json{ "name": "svelte-lib", "version": "1.0.0", "type": "module", "exports": { ".": "./index.js", "./*": "./*" }, "peerDependencies": { "svelte": "^5.0.0" } }lib/index.jslib/Banner.sveltelib/Icon.svelteSteps to reproduce
(cd lib && npm pack --pack-destination ..)npm installnpx vite --force. The output showsFailed to run dependency scanwith[UNRESOLVED_IMPORT] Could not resolve './Icon.svelte' in virtual-module:…/node_modules/svelte-lib/Banner.svelte?id=0.src/App.svelte, switch to the barrel import (import { Banner } from 'svelte-lib') and runnpx vite --forceagain. The scan succeeds andnode_modules/.vite/deps/_metadata.jsonlistssvelte-libunderoptimized.This minimal project reproduces it on vite 8.3.3. The same error appears with vite 8.2.1 in a larger project, and the code is unchanged on
mainat the commit linked above.System Info
System: OS: macOS 27.0.1 CPU: (16) arm64 Apple M4 Max Memory: 2.01 GB / 48.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 26.9.0 - /opt/homebrew/bin/node npm: 11.19.1 - /opt/homebrew/bin/npm pnpm: 12.4.2 - /opt/homebrew/bin/pnpm Deno: 2.9.7 - /opt/homebrew/bin/deno Browsers: Brave Browser: 143.1.85.111 Chrome: 154.0.8037.95 Safari: 27.0.1 npmPackages: @sveltejs/vite-plugin-svelte: ^7.3.0 => 7.3.1 svelte: ^5.56.9 => 5.57.2 vite: 8.3.3 => 8.3.3rolldownis not a direct dependency, so envinfo does not list it.npm ls rolldownreportsrolldown@1.2.12.Used Package Manager
npm
Logs
Click to expand!
Deep import (
import Banner from 'svelte-lib/Banner.svelte'), vite 8.3.3:Barrel import (
import { Banner } from 'svelte-lib'), vite 8.3.3. The scan succeeds, and_metadata.jsonoptimizedincludessvelte-lib:[vite] (client) Forced re-optimization of dependencies [vite] (ssr) Forced re-optimization of dependencies VITE v8.3.3 ready in 310 msDeep import with the fall-through experiment applied to vite 8.3.3. The scan succeeds, and
_metadata.jsonoptimizedincludessvelte-lib/Banner.svelte:[vite] (client) Forced re-optimization of dependencies [vite] (ssr) Forced re-optimization of dependencies VITE v8.3.3 ready in 296 msValidations