Skip to content

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

Merged
RobinMalfait merged 4 commits intotailwindlabs:mainfrom
rebasecase:fix/postcss-basedir-without-opts-from
Apr 26, 2026
Merged

Fall back to the plugin base when PostCSS has no from option#19980
RobinMalfait merged 4 commits intotailwindlabs:mainfrom
rebasecase:fix/postcss-basedir-without-opts-from

Conversation

@rebasecase
Copy link
Copy Markdown
Contributor

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

The PostCSS plugin now falls back to the configured base/cwd when result.opts.from (input file) is missing, preventing incorrect resolution of an empty input path. The change updates the compiler's base input accordingly. A new Vitest covers the case where PostCSS processes @import 'tailwindcss' without a from option and asserts CSS output is produced.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fall back to the plugin base when PostCSS has no from option' is concise, specific, and accurately describes the main change: adding a fallback mechanism when PostCSS lacks a from option.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the bug, the fix, the test plan, and providing code context.
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

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 (1)
packages/@tailwindcss-postcss/src/index.test.ts (1)

143-151: Consider strengthening the assertion to lock in the actual fix.

expect(result.css.length).toBeGreaterThan(0) is a fairly weak guard for this regression: it would still pass if the plugin silently emitted partial output without resolving tailwindcss. Asserting that the @import was actually replaced (e.g. contains a known token from the generated reset/preflight or no longer contains the raw @import 'tailwindcss') more directly verifies that inputBasePath correctly fell back to base and the resolver found tailwindcss.

♻️ Example: tighten the assertion
   let result = await processor.process(`@import 'tailwindcss'`)

-  expect(result.css.length).toBeGreaterThan(0)
+  expect(result.css.length).toBeGreaterThan(0)
+  // Ensure the import was actually resolved (not left as-is) using `base` as fallback.
+  expect(result.css).not.toContain(`@import 'tailwindcss'`)
🤖 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 - 151, The
current assertion only checks result.css.length > 0 which is weak; update the
test "fallback to `base` directory when `result.opts.from` is not provided" to
assert that the import was actually resolved by checking result.css no longer
contains the literal "@import 'tailwindcss'" (or that it contains a known token
from Tailwind’s generated output such as the preflight reset like "*, ::before,
::after" or "preflight" content); modify the assertion on the result produced by
processor.process(...) (variables: processor, result, tailwindcss) to use a more
specific expect (e.g., expect(result.css).not.toContain("@import 'tailwindcss'")
or expect(result.css).toContain("*, ::before, ::after")) so the test verifies
the resolver fell back to the provided base directory.
🤖 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-151: The current assertion only checks result.css.length > 0
which is weak; update the test "fallback to `base` directory when
`result.opts.from` is not provided" to assert that the import was actually
resolved by checking result.css no longer contains the literal "@import
'tailwindcss'" (or that it contains a known token from Tailwind’s generated
output such as the preflight reset like "*, ::before, ::after" or "preflight"
content); modify the assertion on the result produced by processor.process(...)
(variables: processor, result, tailwindcss) to use a more specific expect (e.g.,
expect(result.css).not.toContain("@import 'tailwindcss'") or
expect(result.css).toContain("*, ::before, ::after")) so the test verifies the
resolver fell back to the provided base directory.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d1fee6f7-0f9a-45b9-ba68-a4da39211cef

📥 Commits

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

📒 Files selected for processing (3)
  • CHANGELOG.md
  • packages/@tailwindcss-postcss/src/index.test.ts
  • packages/@tailwindcss-postcss/src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/@tailwindcss-postcss/src/index.ts

Copy link
Copy Markdown
Member

@RobinMalfait RobinMalfait left a comment

Choose a reason for hiding this comment

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

Perfect, thanks!

@RobinMalfait RobinMalfait enabled auto-merge (squash) April 26, 2026 13:41
@RobinMalfait RobinMalfait merged commit bfb5732 into tailwindlabs:main Apr 26, 2026
9 checks passed
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.

2 participants