Skip to content

@tailwindcss/vite discards resolve.conditions when resolving CSS @import, but honours them for JS #20426

Description

@jeffberry

What version of Tailwind CSS are you using?

v4.3.3 (@tailwindcss/vite 4.3.3; also reproduces on 4.1.18)

What build tool are you using?

Vite 8 (the relevant code is unchanged across the plugin's ^5.2.0 || ^6 || ^7 || ^8 peer range)

Describe your issue

@tailwindcss/vite builds two resolvers. The JS one inherits the user's
resolve config as-is; the CSS one spreads that config and then overwrites
conditions with a hardcoded list:

// dist/index.mjs (4.3.3), lightly de-minified
let cssResolver = createIdResolver(config, {
  ...config.resolve,                                  // inherits alias, dedupe, …
  extensions: [".css"],
  mainFields: ["style"],
  conditions: ["style", "development|production"],    // ← replaces user conditions
  tryIndex: false,
  preferRelative: true,
})
let jsResolver = createIdResolver(config, config.resolve)   // ← conditions preserved

Because conditions is assigned after the spread, any custom condition set via
resolve.conditions is silently dropped for CSS @import, while continuing to
work for JS. The two resolvers therefore disagree about which copy of a package
they are looking at.

This is the standard monorepo setup where a library's exports map carries a
custom condition so a workbench/dev app consumes source while published
consumers get the build output:

// packages/my-lib/package.json
{
  "exports": {
    ".":           { "source": "./src/index.ts",  "default": "./dist/index.js" },
    "./style.css": { "source": "./src/style.css", "default": "./dist/style.css" }
  },
  "files": ["dist", "package.json"]
}
// apps/workbench/vite.config.js
export default {
  resolve: { conditions: ["source", ...] },
  plugins: [tailwindcss()],
}
/* apps/workbench/style.css */
@import "tailwindcss";
@import "my-lib/style.css";
/* packages/my-lib/src/style.css — copied verbatim to dist/ at build time */
@import "tailwindcss";
@source "./**/*.{ts,tsx,js}";   /* relative: src/ in the workspace, dist/ once published */

Expected

my-lib/style.css resolves through the source condition to
packages/my-lib/src/style.css, so its relative @source scans src/.

Actual

The source condition is discarded, resolution falls through to default, and
packages/my-lib/dist/style.css is loaded — so @source scans dist/.

The app therefore renders components from src/ while scanning dist/ for
class candidates.
Any utility added since the last build is missing from the
generated CSS, and the only symptom is a component that renders unstyled. It
looks exactly like a broken component rather than a stale-cache problem, so it
is unusually expensive to diagnose — a rebuild makes it disappear, which also
makes it hard to reproduce reliably.

Worth noting there is no way to work around this from the exports map. Adding
a "style" condition (which the hardcoded list would match) does not help:
export conditions cannot distinguish in-repo from published consumption, and
src/ is not in files, so every published consumer would resolve to a path
that does not exist.

Suggested fix

Append rather than replace, so user conditions survive alongside the CSS-specific ones:

conditions: ["style", "development|production", ...(config.resolve.conditions ?? [])],

If overriding is deliberate, it would help to document it — the asymmetry with
the JS resolver two lines away reads as accidental.

Reproduction

Current workaround for anyone hitting this: bypass the export map and import the
library's CSS entry by relative path, so no condition resolution is involved.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions