Skip to content

Fall back to the plugin base when PostCSS has no from option#19980

Open
rebasecase wants to merge 1 commit intotailwindlabs:mainfrom
rebasecase:fix/postcss-basedir-without-opts-from
Open

Fall back to the plugin base when PostCSS has no from option#19980
rebasecase wants to merge 1 commit intotailwindlabs:mainfrom
rebasecase:fix/postcss-basedir-without-opts-from

Conversation

@rebasecase
Copy link
Copy Markdown

Summary

@tailwindcss/postcss derives inputBasePath from result.opts.from:

let inputFile = result.opts.from ?? ''
let inputBasePath = path.dirname(path.resolve(inputFile))

When PostCSS calls the plugin without from (some bundlers, including Turbopack, do this for certain CSS inputs), inputFile is '', path.resolve('') returns process.cwd(), and path.dirname(...) therefore returns the parent of CWD. The downstream compileAst({ base: inputBasePath }) call then asks the resolver to find tailwindcss from one level above the project root, which fails with:

Can't resolve 'tailwindcss' in '<parent of CWD>'

The plugin already computes base = opts.base ?? process.cwd() near the top. Reusing that as the fallback gives a sensible default (CWD) and respects an explicit opts.base when set.

-let inputBasePath = path.dirname(path.resolve(inputFile))
+let inputBasePath = inputFile
+  ? path.dirname(path.resolve(inputFile))
+  : base

Test plan

Added a test in packages/@tailwindcss-postcss/src/index.test.ts that processes @import 'tailwindcss' via processor.process(input) with no from option. Before the fix, this throws Error: Can't resolve 'tailwindcss' in '<parent of CWD>'; after the fix, the import resolves and the processor returns non-empty CSS.

I wasn't able to run the suite locally — pnpm build requires cargo for @tailwindcss/oxide and I don't have a Rust toolchain set up — so the test has been written to match existing conventions in index.test.ts (vitest, plain postcss([tailwindcss({...})]).process(...)), and I'm relying on CI to verify.

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
'<parent of CWD>'".

The plugin already computes `base = opts.base ?? process.cwd()`. Use it as
the fallback so the project root is searched when `from` is missing.
@rebasecase rebasecase requested a review from a team as a code owner April 25, 2026 17:49
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Walkthrough

This change addresses a regression in the Tailwind PostCSS plugin's base path derivation when PostCSS is invoked without a from option. The plugin's compiler configuration previously resolved an empty input file to an incorrect parent directory. The fix implements a fallback mechanism that respects the plugin's configured base option when the from value is missing. A regression test is added to verify that @import 'tailwindcss' resolves correctly when the from option is absent from PostCSS processing.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main fix: falling back to the plugin base when PostCSS lacks a from option, matching the core change in the codebase.
Description check ✅ Passed The description clearly explains the bug, the root cause, the proposed fix with code examples, and the test plan, all directly related to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/@tailwindcss-postcss/src/index.test.ts (1)

143-157: Optional: strengthen the regression assertion.

expect(result.css.length).toBeGreaterThan(0) would also pass if the plugin silently emitted, say, a comment or an unprocessed @import line. Asserting that a known utility from the example-project fixture appears (e.g. .underline, which is referenced in the fixture's index.html) more directly proves that @import 'tailwindcss' resolved against the configured base and that scanning ran. It also guards against future regressions where the import silently no-ops.

♻️ Suggested assertion
-  let result = await processor.process(`@import 'tailwindcss'`)
-
-  expect(result.css.length).toBeGreaterThan(0)
+  let result = await processor.process(`@import 'tailwindcss'`)
+
+  expect(result.css.length).toBeGreaterThan(0)
+  expect(result.css).toContain('.underline')

Also, organizationally this case fits naturally inside the existing describe('processing without specifying a base path', ...) block (or a sibling describe), but that's purely cosmetic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/`@tailwindcss-postcss/src/index.test.ts around lines 143 - 157,
Update the test "processing input without a `from` option falls back to the
plugin `base`" to assert a concrete utility from the example-project fixture
rather than only checking result length: replace the loose assertion
expect(result.css.length).toBeGreaterThan(0) with a check that the generated CSS
contains a known class (for example assert result.css includes ".underline" or
another utility present in the fixtures) so the test verifies that the import
resolved and scanning ran; optionally move this test into the existing
describe('processing without specifying a base path', ...) block for
organization.
packages/@tailwindcss-postcss/src/index.ts (1)

185-206: Optional: avoid forcing a full rebuild on every no-from invocation.

When inputFile === '', line 189 still pushes the empty string into files. Then at line 192, fs.statSync('', { throwIfNoEntry: false }) yields null, the file === inputFile branch matches, and rebuildStrategy is forced to 'full' on every subsequent invocation — defeating the mtime-based cache for exactly the Turbopack/no-from path this PR enables. Pre-existing, but now reachable in a supported flow.

♻️ Proposed tweak
-              files.push(inputFile)
+              if (inputFile) files.push(inputFile)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/`@tailwindcss-postcss/src/index.ts around lines 185 - 206, The code
currently pushes inputFile (which can be the empty string for no-`from`
invocations) into files and then treats a stat failure of that empty path as
forcing rebuildStrategy = 'full'; update the logic in the block that builds
"files" (and/or just before iterating) to exclude empty/falsey filenames so that
inputFile === '' is not pushed or processed: either only push inputFile when it
is a non-empty string, or filter files to remove '' before the fs.statSync loop,
leaving the existing file === inputFile branch unchanged for real files;
reference symbols: files, inputFile, result.messages, context.mtimes,
rebuildStrategy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/`@tailwindcss-postcss/src/index.test.ts:
- Around line 143-157: Update the test "processing input without a `from` option
falls back to the plugin `base`" to assert a concrete utility from the
example-project fixture rather than only checking result length: replace the
loose assertion expect(result.css.length).toBeGreaterThan(0) with a check that
the generated CSS contains a known class (for example assert result.css includes
".underline" or another utility present in the fixtures) so the test verifies
that the import resolved and scanning ran; optionally move this test into the
existing describe('processing without specifying a base path', ...) block for
organization.

In `@packages/`@tailwindcss-postcss/src/index.ts:
- Around line 185-206: The code currently pushes inputFile (which can be the
empty string for no-`from` invocations) into files and then treats a stat
failure of that empty path as forcing rebuildStrategy = 'full'; update the logic
in the block that builds "files" (and/or just before iterating) to exclude
empty/falsey filenames so that inputFile === '' is not pushed or processed:
either only push inputFile when it is a non-empty string, or filter files to
remove '' before the fs.statSync loop, leaving the existing file === inputFile
branch unchanged for real files; reference symbols: files, inputFile,
result.messages, context.mtimes, rebuildStrategy.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2c32dbe5-6989-47a4-bd56-a1acedd1cd4f

📥 Commits

Reviewing files that changed from the base of the PR and between 3a890c3 and c36ac98.

📒 Files selected for processing (2)
  • packages/@tailwindcss-postcss/src/index.test.ts
  • packages/@tailwindcss-postcss/src/index.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant