Vite Refuses a CSS Import from `node_modules` with "does not exist under root" During Build
vite build fails on a stylesheet import such as import 'some-package/dist/style.css' with [vite:css] [postcss] ENOENT: no such file or directory, open '.../node_modules/.../dist/style.css' or does not exist under root, depending on the path and Vite version. The import resolves during development, then the production build rejects it because the file is outside the allowed root or outside the package’s published entry points.
What the error means
Vite does not treat the project as an unrestricted filesystem. It serves source files from the configured root and resolves package imports through its dependency pipeline. That distinction is important.
A path like import 'some-package/dist/style.css' can fail for two related reasons:
- The file is not part of the package’s public entry surface, so the package’s resolver does not expose it as a stable import target.
- The resolved file lives outside the area Vite allows the app to import directly during build, especially when the path points above
rootor into a location Vite does not consider source.
The exact error often looks like this:
text[vite:css] [postcss] ENOENT: no such file or directory, open '/path/to/project/node_modules/some-package/dist/style.css'
Or this:
textThe file does not exist under root: /path/to/project/node_modules/some-package/dist/style.css
The underlying issue is not CSS-specific. The same restriction applies to any asset Vite has to read as a file, including images, fonts, and raw text.
Why it can work in development and fail in build
Vite development mode and Vite build do not use the same execution path.
In dev, Vite acts as a file server with on-demand transforms. When a browser requests a module or asset, Vite resolves it, applies plugins, and serves the result. Some package subpaths appear to work because the dev server is permissive enough to read the file once the import graph points at it.
In build, Vite performs a full static scan of the module graph and applies stricter resolution rules. It needs a closed set of inputs so Rollup can generate the final bundle. That scan relies on package entry points, declared exports, and file paths that Vite can safely track.
This difference is why a package asset path may appear valid in dev but fail during production build:
- Dev can read a deep file path if it is reachable from a browser module request.
- Build must statically resolve every import before bundling.
- A deep path into
node_modulesmay not be an exported subpath. - A file outside the project root or Vite allowlist is rejected.
How Vite resolves package CSS
A package import such as import 'some-package' goes through the package entry point. If the package author ships CSS through that entry point, Vite can discover it and include it in the graph.
For example, many libraries expose CSS like this in package.json:
json{ "name": "some-package", "exports": { ".": "./dist/index.js", "./style.css": "./dist/style.css" }, "style": "./dist/style.css" }
In that case, the supported import is typically:
tsimport 'some-package/style.css'
or sometimes simply:
tsimport 'some-package'
if the package’s JS entry imports its own CSS.
A deep import like import 'some-package/dist/style.css' only works if that exact subpath is published and allowed. Many packages do not guarantee that path, even if the file exists in the installed tarball. The package can change internal layout without semver guarantees, and Vite’s resolver may block the access during build.
The preferred fix: import through the package entry point
If the package provides a documented CSS entry, use it.
Example
tsimport 'some-package/style.css'
If the package’s main entry already includes the stylesheet, import the package once:
tsimport 'some-package'
This is the most stable fix because it uses the package’s public API instead of a private filesystem path.
Why this works
Vite recognizes package entry points and package subpaths declared in exports. That means the resolver can track the asset as part of the dependency graph instead of treating it as an arbitrary file read from node_modules.
If the package does not document a CSS entry and only exposes a deep path, check its package.json and published files first. If there is no exported style path, the package is not promising that import will stay usable.
When server.fs.allow is relevant
server.fs.allow only affects the dev server’s file system access rules. It does not make an unsupported production import valid.
Use it only when the app genuinely needs to serve files from outside the project root during development, such as a monorepo workspace or a local package linked with pnpm, npm link, or yarn link.
Example vite.config.ts
tsimport { defineConfig } from 'vite' import path from 'node:path' export default defineConfig({ server: { fs: { allow: [ path.resolve(__dirname, '..') ] } } })
This allows the dev server to read files from the parent directory. It does not change how vite build treats package assets.
When not to use it
Do not use server.fs.allow to paper over a bad package import such as a deep path inside node_modules. If the package path is not exported or not meant to be consumed directly, allowing more of the filesystem only hides the real problem. Build-time resolution still needs a valid import target.
Copy the asset into the app source tree when it is not a package API
If the CSS file is not a supported package entry and you still need that exact stylesheet, copy it into your app’s source tree and import the local copy.
This is the right move when:
- the package does not export the CSS path,
- the file is generated or patched locally,
- the package is third-party but you need to vendor a specific asset,
- or the asset needs to be versioned with the app.
Example
Copy the file:
bashmkdir -p src/vendor/some-package cp node_modules/some-package/dist/style.css src/vendor/some-package/style.css
Then import it from src:
tsimport './vendor/some-package/style.css'
Because the file now sits under the app root and source tree, Vite can include it in the normal build pipeline.
How to verify what the package actually exports
Before changing app code, inspect the installed package metadata.
Check the package exports
bashcat node_modules/some-package/package.json
Look for exports, style, or documented entry files. If exports exists, deep paths outside it may be blocked.
Check whether the CSS file is published
bashls -la node_modules/some-package ls -la node_modules/some-package/dist
If the file is present but not exported, that still does not mean it is a supported import target.
Check whether the package imports its own CSS
Search the package entry:
bashgrep -R "style.css" node_modules/some-package -n
If the main JS entry already pulls in the stylesheet, importing the package root is usually enough.
Build-time behavior to keep in mind
Vite build is usually run with a command like:
bashnpm run build
where package.json contains:
json{ "scripts": { "build": "vite build" } }
The production pipeline runs through Rollup and Vite plugins, so import resolution needs to be deterministic. That means the build has to know, at scan time, which files are valid sources.
A path that escapes the project root, or a path that only works because the dev server happened to serve it, is a liability in build mode. Vite is not guessing what you meant. It is enforcing the graph that can actually be bundled.
Typical failure patterns
A few import forms are commonly involved:
Deep package asset import
tsimport 'some-package/dist/style.css'
This is fragile unless the package explicitly exports that subpath.
Relative path into node_modules
tsimport '../node_modules/some-package/dist/style.css'
This tends to fail because it bypasses package resolution and interacts poorly with the root boundary.
Importing a file outside source without allowlist support
tsimport '/absolute/path/to/shared/style.css'
This can be blocked unless the file is within root or explicitly allowed by the dev server, and it still needs to be a valid build input.
Diagnosing the root cause quickly
Use a simple checklist.
- Find the exact import statement that references the CSS file.
- Check whether the path is a documented package entry or just an internal file.
- Inspect the package
package.jsonforexportsorstyle. - Confirm whether the file is under the app root or only reachable through
node_modules. - If it is a linked workspace file, decide whether
server.fs.allowis appropriate for dev only. - If the asset is not a supported package export, vendor it into
src.
For example, if the app contains:
tsimport 'some-package/dist/style.css'
and the package metadata only exposes:
json{ "exports": { ".": "./dist/index.js", "./style.css": "./dist/style.css" } }
then the import should become:
tsimport 'some-package/style.css'
If style.css is not exported at all, copy it locally instead.
Example vite.config.ts for a workspace setup
A monorepo can require broader dev-server access while still keeping production imports normal.
tsimport { defineConfig } from 'vite' import path from 'node:path' export default defineConfig({ server: { fs: { allow: [ path.resolve(__dirname, '..'), path.resolve(__dirname, '../shared') ] } } })
This is appropriate when importing a shared workspace package during development. It is not a fix for a package asset path that build cannot legally resolve.
Example app code after fixing the import
ts// src/main.ts import { createApp } from 'vue' import App from './App.vue' import 'some-package/style.css' createApp(App).mount('#app')
If the stylesheet is vendored locally instead:
ts// src/main.ts import { createApp } from 'vue' import App from './App.vue' import './vendor/some-package/style.css' createApp(App).mount('#app')
Practical takeaway
Prefer importing CSS through the package’s documented entry point, such as import 'some-package/style.css' or import 'some-package', because that matches Vite’s package resolution and survives build. Use server.fs.allow only for legitimate workspace or linked-file access in development. If the asset is not part of the package’s public API, copy it into src and import the local file so the build sees a normal project source file.