Vite Refuses a CSS Import from `node_modules` with "does not exist under root" During Build

assets, build, css, node-modules, vite

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:

  1. 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.
  2. The resolved file lives outside the area Vite allows the app to import directly during build, especially when the path points above root or 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:

text
The 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:

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:

ts
import 'some-package/style.css'

or sometimes simply:

ts
import '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

ts
import 'some-package/style.css'

If the package’s main entry already includes the stylesheet, import the package once:

ts
import '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

ts
import { 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:

Example

Copy the file:

bash
mkdir -p src/vendor/some-package cp node_modules/some-package/dist/style.css src/vendor/some-package/style.css

Then import it from src:

ts
import './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

bash
cat 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

bash
ls -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:

bash
grep -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:

bash
npm 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

ts
import 'some-package/dist/style.css'

This is fragile unless the package explicitly exports that subpath.

Relative path into node_modules

ts
import '../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

ts
import '/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.

  1. Find the exact import statement that references the CSS file.
  2. Check whether the path is a documented package entry or just an internal file.
  3. Inspect the package package.json for exports or style.
  4. Confirm whether the file is under the app root or only reachable through node_modules.
  5. If it is a linked workspace file, decide whether server.fs.allow is appropriate for dev only.
  6. If the asset is not a supported package export, vendor it into src.

For example, if the app contains:

ts
import '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:

ts
import '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.

ts
import { 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.