Skip to content

fix(vite-plugin-angular): scope style package-export condition to .css imports#2330

Merged
brandonroberts merged 2 commits intoalphafrom
fix/scoped-css-style-resolver
Apr 29, 2026
Merged

fix(vite-plugin-angular): scope style package-export condition to .css imports#2330
brandonroberts merged 2 commits intoalphafrom
fix/scoped-css-style-resolver

Conversation

@brandonroberts
Copy link
Copy Markdown
Member

PR Checklist

The Angular Vite plugin injected the style package-export condition into Vite's global resolve.conditions (in angular-vite-plugin, fast-compile-plugin, compilation-api-plugin, and the shared createDepOptimizerConfig). That global injection was needed so JS-side imports of CSS whose package exports gate them only under style — e.g. import '@angular/material/prebuilt-themes/azure-blue.css' — could resolve at all.

But globalising style had a load-bearing side effect: Tailwind v4's @plugin JS resolver inherits Vite's resolve.conditions. With style in scope, Tailwind matched the style export of packages with mixed exports (notably tailwindcss-primeui, whose . exports include both style: "./v4/index.css" and import: "./v3/index.js") and handed the resolved .css file to Node's ESM loader, which crashed with:

Internal server error: Unknown file extension ".css" for
  …/tailwindcss-primeui/v4/index.css

Removing the style injection outright (as analogjs/analog#2326 proposes) trades one regression for another: it breaks @angular/material/prebuilt-themes and any other package that exposes its CSS only under style.

Closes #

Affected scope

  • Primary scope: vite-plugin-angular
  • Secondary scopes: none

Recommended merge strategy for maintainer [optional]

  • Squash merge
  • Rebase merge
  • Other

Commit preservation note [optional]

N/A — single focused change.

What is the new behavior?

style is no longer added to Vite's global resolve.conditions. Instead, a new cssExtensionStyleResolverPlugin is registered once at the angular() factory level. It builds a scoped resolver (via Vite 8's vite.createIdResolver or Vite 7's ResolvedConfig.createResolver) with style appended to the user's configured conditions, and only fires when the requested id is a bare specifier ending in .css (with optional ?inline / ?module query).

Net effect:

  • import '@angular/material/prebuilt-themes/azure-blue.css'
  • @import '@angular/material/prebuilt-themes/azure-blue.css'
  • import '@angular/material/button' (non-CSS) ✓
  • @plugin 'tailwindcss-primeui' (Tailwind JS resolver) ✓

The shared createDepOptimizerConfig no longer returns a global resolve block, and the per-plugin global resolve.conditions: ['style', ...] injection is removed from angular-vite-plugin, fast-compile-plugin, and compilation-api-plugin.

Test plan

  • cssExtensionStyleResolverPlugin unit tests (new, 6 cases) cover fire/no-fire cases, condition appending, and both Vite version paths via a vi.mock('vite', …)-backed resolver factory
  • createDepOptimizerConfig test asserts the helper no longer returns a global resolve block
  • nx format:check
  • pnpm build
  • pnpm test
  • Manual verification — confirm both @angular/material/prebuilt-themes/azure-blue.css resolves and a Tailwind v4 app using @plugin 'tailwindcss-primeui' no longer crashes the ESM loader

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

Alternative to analogjs/analog#2326, which removes the style injection outright. This PR keeps Material-style packages working by scoping style to .css requests rather than dropping it.

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 29, 2026

Deploy Preview for analog-app ready!

Name Link
🔨 Latest commit b190e0d
🔍 Latest deploy log https://app.netlify.com/projects/analog-app/deploys/69f2317e8d0349000842e17f
😎 Deploy Preview https://deploy-preview-2330--analog-app.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the scope:vite-plugin-angular Changes in @analogjs/vite-plugin-angular label Apr 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes remove code that globally mutated Vite’s resolve.conditions to prepend 'style' during dep optimization and compilation. A new plugin, cssExtensionStyleResolverPlugin(), is added and registered early; it constructs a resolver that appends the 'style' condition only for bare .css-extension imports (including query-suffixed variants). Compilation, fast-compile, and dep-optimizer config paths no longer modify resolve.conditions. New Vitest suites verify the scoped resolver behavior and ensure dep-optimizer config does not expose a top-level resolve block.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Key observations

  • Behavioral change: Global 'style' condition is removed; code that relied on a globally-present 'style' condition outside .css-extension imports may break — treat as a behavioral change per CONTRIBUTING.md.
  • Vite API compatibility: Resolver adapts to Vite 8 (createIdResolver) and Vite 7 (ResolvedConfig.createResolver) and throws when neither exists; ensure the error message is clear and consider tests for unsupported Vite versions.
  • Plugin ordering: The new resolver is registered to run early; confirm overall plugin composition preserves intended ordering and no other early plugin bypasses scoped resolution.
  • Test coverage: Unit tests cover resolver creation, .css-only matching (including query suffixes), and dep-optimizer leakage. Missing are integration tests exercising the resolver within full Vite runs and cases with mixed JS/CSS exports (e.g., Tailwind v4).
  • Regression surface: Removal of resolve.conditions overrides touches angular factory registration, compilation-api, fast-compile, and dep-optimizer config — review all call sites and any third-party assumptions about a global 'style' condition.
  • Maintenance: Multi-API branching for Vite compatibility increases maintenance burden; add CI or test matrix across supported Vite versions.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows Conventional Commit format with supported scope and clearly describes the main change: scoping the style condition to CSS imports.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, implementation approach, and test coverage for scoping style to CSS imports.
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
Review rate limit: 6/8 reviews remaining, refill in 8 minutes and 39 seconds.

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

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 29, 2026

Deploy Preview for analog-blog ready!

Name Link
🔨 Latest commit b190e0d
🔍 Latest deploy log https://app.netlify.com/projects/analog-blog/deploys/69f2317eedf71d0008e197a6
😎 Deploy Preview https://deploy-preview-2330--analog-blog.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

…css imports

The Angular Vite plugin injected the `style` package-export condition
into Vite's *global* `resolve.conditions` (in `angular-vite-plugin`,
`fast-compile-plugin`, `compilation-api-plugin`, and the shared
`createDepOptimizerConfig`). That global injection was needed so JS-side
imports of CSS files whose package exports gate them only under `style`
— e.g. `import '@angular/material/prebuilt-themes/azure-blue.css'` —
could resolve at all.

But globalising `style` had a load-bearing side effect: Tailwind v4's
`@plugin` JS resolver inherits Vite's `resolve.conditions`. With `style`
in scope, Tailwind matched the `style` export of packages with mixed
exports (notably `tailwindcss-primeui`, whose `.` exports include both
`style: "./v4/index.css"` and `import: "./v3/index.js"`) and handed the
resolved `.css` file to Node's ESM loader, which crashed with:

    Internal server error: Unknown file extension ".css" for
      …/tailwindcss-primeui/v4/index.css

Removing the `style` injection outright (as PR #2326 proposes) trades
one regression for another: it breaks `@angular/material/prebuilt-themes`
and any other package that exposes its CSS only under `style`.

This commit scopes the `style` condition to `.css`-extension requests
via a new `cssExtensionStyleResolverPlugin`, registered once at the
`angular()` factory level. The plugin builds its own scoped resolver
(via Vite 8's `vite.createIdResolver` or Vite 7's
`ResolvedConfig.createResolver`) with `style` appended to the user's
configured conditions, and only fires when the requested id is a bare
specifier ending in `.css` (with optional `?inline` / `?module` query).

Net effect:

  - `import '@angular/material/prebuilt-themes/azure-blue.css'`     ✓
  - `@import '@angular/material/prebuilt-themes/azure-blue.css'`    ✓
  - `import '@angular/material/button'` (non-CSS)                   ✓
  - `@plugin 'tailwindcss-primeui'` (Tailwind JS resolver)          ✓

Tests:
  - `cssExtensionStyleResolverPlugin` (new, 6 cases) covers the
    fire/no-fire cases, condition appending, and both Vite version paths
    via a `vi.mock('vite', …)`-backed resolver factory.
  - `createDepOptimizerConfig` test asserts the helper no longer
    returns a global `resolve` block.
@brandonroberts brandonroberts force-pushed the fix/scoped-css-style-resolver branch from 253e75e to 1ede6ba Compare April 29, 2026 16:14
Copy link
Copy Markdown

@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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts (1)

12-35: Add coverage for the config.createResolver fallback.

Every test in this file forces the vite.createIdResolver path, so the fallback branch in cssExtensionStyleResolverPlugin() never executes. That leaves the progressive-compatibility path unguarded by CI.

As per coding guidelines, {vite.config.ts,packages/vite-plugin-*/**/*.ts}: Maintain compatibility with Vite versions 6-8, with progressive fallbacks.

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

In `@packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts`
around lines 12 - 35, The tests currently always exercise the Vite 8 path by
mocking createIdResolver, so add a new test that forces the progressive-fallback
branch by ensuring the plugin sees config.createResolver instead of
vite.createIdResolver: create a test that does not mock createIdResolver (or
adjusts the existing vi.mock to omit createIdResolver), instantiate
cssExtensionStyleResolverPlugin() with a fake Vite config object that includes a
createResolver function, call the resolver returned by
cssExtensionStyleResolverPlugin and assert that your fake config.createResolver
was invoked and returned the expected resolution; reference the
cssExtensionStyleResolverPlugin factory, the vite.createIdResolver override in
the current test file, and the config.createResolver fallback so the new test
covers that branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts`:
- Around line 115-125: The early-return guard for non-bare specifiers misses
Windows drive-letter absolute paths like "C:/..." so such absolute stylesheet
ids are wrongly treated as package specifiers; update the condition in
css-extension-resolver (the block that checks
id.startsWith('.')|'/'|'\0'|'data:'|'virtual:') to also detect Windows drive
letters (e.g. id matches /^[A-Za-z]:\// after Vite's POSIX-normalization) and
return null for those ids as well so resolveCss() is not invoked for absolute
Windows paths.

---

Nitpick comments:
In `@packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts`:
- Around line 12-35: The tests currently always exercise the Vite 8 path by
mocking createIdResolver, so add a new test that forces the progressive-fallback
branch by ensuring the plugin sees config.createResolver instead of
vite.createIdResolver: create a test that does not mock createIdResolver (or
adjusts the existing vi.mock to omit createIdResolver), instantiate
cssExtensionStyleResolverPlugin() with a fake Vite config object that includes a
createResolver function, call the resolver returned by
cssExtensionStyleResolverPlugin and assert that your fake config.createResolver
was invoked and returned the expected resolution; reference the
cssExtensionStyleResolverPlugin factory, the vite.createIdResolver override in
the current test file, and the config.createResolver fallback so the new test
covers that branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1c9ffc92-17f3-4d93-87a6-32ac9538c5ee

📥 Commits

Reviewing files that changed from the base of the PR and between 39b5a4a and 253e75e.

📒 Files selected for processing (7)
  • packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
  • packages/vite-plugin-angular/src/lib/compilation-api/compilation-api-plugin.ts
  • packages/vite-plugin-angular/src/lib/fast-compile-plugin.ts
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts
  • packages/vite-plugin-angular/src/lib/utils/plugin-config.spec.ts
  • packages/vite-plugin-angular/src/lib/utils/plugin-config.ts

Copy link
Copy Markdown

@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/vite-plugin-angular/src/lib/angular-vite-plugin.ts (1)

1555-1562: Good plugin placement; add a wiring-order regression test.

cssExtensionStyleResolverPlugin() being registered at angular() level is the right integration point. Consider adding/keeping a focused test that asserts this plugin is present and ordered before compilation plugins to prevent future regressions.

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

In `@packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts` around lines
1555 - 1562, Add a regression test that ensures
cssExtensionStyleResolverPlugin() is registered when angular() is invoked and
that it appears before compilation plugins (e.g., the plugin that handles
TypeScript/compilation) to prevent wiring-order regressions; locate the
angular() setup where cssExtensionStyleResolverPlugin() and replaceFiles(...)
are added and write a focused test that instantiates angular() with minimal
pluginOptions, inspects the resulting plugin list or resolved hook ordering, and
asserts cssExtensionStyleResolverPlugin() is present and comes before the
compilation plugin(s).
🤖 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/vite-plugin-angular/src/lib/angular-vite-plugin.ts`:
- Around line 1555-1562: Add a regression test that ensures
cssExtensionStyleResolverPlugin() is registered when angular() is invoked and
that it appears before compilation plugins (e.g., the plugin that handles
TypeScript/compilation) to prevent wiring-order regressions; locate the
angular() setup where cssExtensionStyleResolverPlugin() and replaceFiles(...)
are added and write a focused test that instantiates angular() with minimal
pluginOptions, inspects the resulting plugin list or resolved hook ordering, and
asserts cssExtensionStyleResolverPlugin() is present and comes before the
compilation plugin(s).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ec2f959e-5758-4127-9028-23706b7c5b33

📥 Commits

Reviewing files that changed from the base of the PR and between 253e75e and 1ede6ba.

📒 Files selected for processing (7)
  • packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts
  • packages/vite-plugin-angular/src/lib/compilation-api/compilation-api-plugin.ts
  • packages/vite-plugin-angular/src/lib/fast-compile-plugin.ts
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts
  • packages/vite-plugin-angular/src/lib/utils/plugin-config.spec.ts
  • packages/vite-plugin-angular/src/lib/utils/plugin-config.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/vite-plugin-angular/src/lib/utils/plugin-config.spec.ts
  • packages/vite-plugin-angular/src/lib/utils/plugin-config.ts
  • packages/vite-plugin-angular/src/lib/compilation-api/compilation-api-plugin.ts
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts

…css resolver

Vite POSIX-normalizes resolved ids on Windows but preserves the drive
volume, so an absolute stylesheet can reach `cssExtensionStyleResolverPlugin`
as `C:/...` rather than `/...`. The early-return guard only checked for
`/`, so Windows absolute paths slipped through and were forwarded to the
package-exports resolver as if they were bare specifiers. Match Windows
drive letters (`/^[A-Za-z]:\//`) and bail out early.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts`:
- Around line 100-109: Update the thrown Error message in the resolver fallback
block (where vite.createIdResolver and ResolvedConfig.createResolver are
checked) to state that Vite 6–8 are supported and that neither API being present
indicates an unsupported or misconfigured Vite version; keep references to
vite.createIdResolver and ResolvedConfig.createResolver in the message so it
clearly communicates the two APIs being checked and that Vite 6–8 are expected
to provide at least one of them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e2f4721d-3592-4950-9dcd-c7be9bdc1335

📥 Commits

Reviewing files that changed from the base of the PR and between 1ede6ba and b190e0d.

📒 Files selected for processing (2)
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.spec.ts
  • packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts

Comment on lines +100 to +109
// Both APIs have been stable across the supported Vite range
// (7.x via `ResolvedConfig.createResolver`, 8.x via
// `vite.createIdResolver`). If neither is present, fail loudly
// rather than silently regressing CSS imports of `style`-only
// package exports such as `@angular/material/prebuilt-themes/*`.
throw new Error(
'[@analogjs/vite-plugin-angular]: neither vite.createIdResolver ' +
'(Vite 8) nor ResolvedConfig.createResolver (Vite 7) is available. ' +
'Unsupported Vite version.',
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Vite 6 expose createResolver on ResolvedConfig or createIdResolver?

💡 Result:

In Vite 6, ResolvedConfig exposes createResolver, but it is deprecated. Plugins should use createIdResolver(environment.config) instead, where createIdResolver is imported from 'vite'. The createResolver method on ResolvedConfig only works for 'client' and 'ssr' environments and ignores the aliasOnly option.

Citations:


🏁 Script executed:

cat -n packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts | sed -n '90,115p'

Repository: analogjs/analog

Length of output: 1236


🏁 Script executed:

cat -n packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts | sed -n '75,110p'

Repository: analogjs/analog

Length of output: 1534


🏁 Script executed:

cat -n packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts | sed -n '60,110p'

Repository: analogjs/analog

Length of output: 2108


Update error message to reflect full Vite 6-8 support.

The code correctly supports Vite 6-8 via progressive fallback: first attempting vite.createIdResolver (Vite 8+), then falling back to ResolvedConfig.createResolver (Vite 6-7+). However, the error message at line 105-109 states support only for "Vite 7 and 8" and omits Vite 6. Since Vite 6 exposes ResolvedConfig.createResolver per the web search, update the message to accurately reflect that all three versions (6, 7, 8) are expected to provide at least one of these APIs, clarifying that the error indicates an unexpectedly misconfigured or unsupported Vite version altogether.

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

In `@packages/vite-plugin-angular/src/lib/utils/css-extension-resolver.ts` around
lines 100 - 109, Update the thrown Error message in the resolver fallback block
(where vite.createIdResolver and ResolvedConfig.createResolver are checked) to
state that Vite 6–8 are supported and that neither API being present indicates
an unsupported or misconfigured Vite version; keep references to
vite.createIdResolver and ResolvedConfig.createResolver in the message so it
clearly communicates the two APIs being checked and that Vite 6–8 are expected
to provide at least one of them.

@brandonroberts brandonroberts merged commit 0920b62 into alpha Apr 29, 2026
27 of 33 checks passed
@brandonroberts brandonroberts deleted the fix/scoped-css-style-resolver branch April 29, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope:vite-plugin-angular Changes in @analogjs/vite-plugin-angular

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant