From c36ac98ce7852af10bcd76873a837bd504fe5530 Mon Sep 17 00:00:00 2001 From: rebasecase Date: Sat, 25 Apr 2026 18:48:41 +0100 Subject: [PATCH 1/4] Fall back to the plugin base when PostCSS has no `from` option When PostCSS invokes the plugin without `result.opts.from` (some bundlers, including Turbopack, do this for certain CSS inputs), `inputFile` defaults to `''`, and `path.dirname(path.resolve(''))` resolves to the *parent* of `process.cwd()` rather than CWD. That made `@import 'tailwindcss'` walk above the project root and fail with "Can't resolve 'tailwindcss' in ''". The plugin already computes `base = opts.base ?? process.cwd()`. Use it as the fallback so the project root is searched when `from` is missing. --- packages/@tailwindcss-postcss/src/index.test.ts | 16 ++++++++++++++++ packages/@tailwindcss-postcss/src/index.ts | 11 ++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index ff12e09ef6af..3f2bfc86f192 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -140,6 +140,22 @@ describe('processing without specifying a base path', () => { }) }) +test('processing input without a `from` option falls back to the plugin `base`', async () => { + // PostCSS may invoke plugins without `result.opts.from` (some bundlers, + // including Turbopack, do this for certain CSS inputs). When that happens + // `path.dirname(path.resolve(''))` yields the *parent* of CWD, which made + // `@import 'tailwindcss'` resolution walk above the project root and fail + // with "Can't resolve 'tailwindcss' in ''". The fallback + // should be the plugin-level `base` (which itself defaults to CWD). + let processor = postcss([ + tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), + ]) + + let result = await processor.process(`@import 'tailwindcss'`) + + expect(result.css.length).toBeGreaterThan(0) +}) + describe('plugins', () => { test('local CJS plugin', async () => { let processor = postcss([ diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 2ff4405b66b6..4180926a9df0 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -115,7 +115,16 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } let context = getContextFromCache(postcss, inputFile, opts) - let inputBasePath = path.dirname(path.resolve(inputFile)) + // When PostCSS is invoked without `from` (some bundlers, including + // Turbopack, do this for certain inputs), `inputFile` is `''`. + // `path.resolve('')` returns `process.cwd()`, so `path.dirname(...)` + // would fall back to the *parent* of CWD — wrong, and breaks + // `@import 'tailwindcss'` resolution against the project's + // `node_modules`. Fall back to the plugin-level `base` (which + // already defaults to `process.cwd()` and respects `opts.base`). + let inputBasePath = inputFile + ? path.dirname(path.resolve(inputFile)) + : base // Whether this is the first build or not, if it is, then we can // optimize the build by not creating the compiler until we need it. From 5082576c9125c9680f10d10604af5cb0512c0795 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 26 Apr 2026 12:57:16 +0200 Subject: [PATCH 2/4] format --- packages/@tailwindcss-postcss/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index 4180926a9df0..d9caf7eb89d3 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -122,9 +122,7 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { // `@import 'tailwindcss'` resolution against the project's // `node_modules`. Fall back to the plugin-level `base` (which // already defaults to `process.cwd()` and respects `opts.base`). - let inputBasePath = inputFile - ? path.dirname(path.resolve(inputFile)) - : base + let inputBasePath = inputFile ? path.dirname(path.resolve(inputFile)) : base // Whether this is the first build or not, if it is, then we can // optimize the build by not creating the compiler until we need it. From 44823211dc9c9416f09e15fd1c9363b0d34e9904 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 26 Apr 2026 15:37:04 +0200 Subject: [PATCH 3/4] re-word test + remove unnecessary comments --- packages/@tailwindcss-postcss/src/index.test.ts | 8 +------- packages/@tailwindcss-postcss/src/index.ts | 7 ------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/@tailwindcss-postcss/src/index.test.ts b/packages/@tailwindcss-postcss/src/index.test.ts index 3f2bfc86f192..9d23d0a8ded0 100644 --- a/packages/@tailwindcss-postcss/src/index.test.ts +++ b/packages/@tailwindcss-postcss/src/index.test.ts @@ -140,13 +140,7 @@ describe('processing without specifying a base path', () => { }) }) -test('processing input without a `from` option falls back to the plugin `base`', async () => { - // PostCSS may invoke plugins without `result.opts.from` (some bundlers, - // including Turbopack, do this for certain CSS inputs). When that happens - // `path.dirname(path.resolve(''))` yields the *parent* of CWD, which made - // `@import 'tailwindcss'` resolution walk above the project root and fail - // with "Can't resolve 'tailwindcss' in ''". The fallback - // should be the plugin-level `base` (which itself defaults to CWD). +test('fallback to `base` directory when `result.opts.from` is not provided', async () => { let processor = postcss([ tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }), ]) diff --git a/packages/@tailwindcss-postcss/src/index.ts b/packages/@tailwindcss-postcss/src/index.ts index d9caf7eb89d3..0bc70cb9cf94 100644 --- a/packages/@tailwindcss-postcss/src/index.ts +++ b/packages/@tailwindcss-postcss/src/index.ts @@ -115,13 +115,6 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin { } let context = getContextFromCache(postcss, inputFile, opts) - // When PostCSS is invoked without `from` (some bundlers, including - // Turbopack, do this for certain inputs), `inputFile` is `''`. - // `path.resolve('')` returns `process.cwd()`, so `path.dirname(...)` - // would fall back to the *parent* of CWD — wrong, and breaks - // `@import 'tailwindcss'` resolution against the project's - // `node_modules`. Fall back to the plugin-level `base` (which - // already defaults to `process.cwd()` and respects `opts.base`). let inputBasePath = inputFile ? path.dirname(path.resolve(inputFile)) : base // Whether this is the first build or not, if it is, then we can From b69ca40b2cdd8665bda0ec4f470b21f5c48706ab Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 26 Apr 2026 15:38:56 +0200 Subject: [PATCH 4/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b21eed24d8..2768ffbc4f44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `@plugin` resolves package JavaScript entries instead of browser CSS entries when using `@tailwindcss/vite` ([#19949](https://github.com/tailwindlabs/tailwindcss/pull/19949)) - Fix relative `@import` and `@plugin` paths resolving from the wrong directory when using `@tailwindcss/vite` ([#19965](https://github.com/tailwindlabs/tailwindcss/pull/19965)) - Ensure CSS files containing `@variant` are processed by `@tailwindcss/vite` ([#19966](https://github.com/tailwindlabs/tailwindcss/pull/19966)) +- Resolve imports relative to `base` when `result.opts.from` is not provided when using `@tailwindcss/postcss` ([#19980](https://github.com/tailwindlabs/tailwindcss/pull/19980)) ## [4.2.4] - 2026-04-21