Skip to content

@tailwindcss/vite forces full reload for Angular component stylesheet edits that are later handled by CSS HMR #19903

@benpsnyder

Description

@benpsnyder

Tracking

Item Link
Framework-side investigation and repro harness analogjs/analog#2226 (comment)
Proposed upstream fix #19904

Environment

Question Answer
Tailwind version @tailwindcss/vite v4.2.2
Build tool / framework Analog.js on Vite 8.0.3 with Angular 21 component stylesheets
Node.js v25.8.2 locally for investigation
Browser Chrome
Operating system macOS

Reproduction

Item Value
Public repro Branch-based repro harness: https://github.com/benpsnyder/analog/tree/fix/tailwind
Private repro characteristics Vite 8, @tailwindcss/vite 4.2.2, Analog.js / Angular component stylesheet HMR
Trigger edit a component .css file
Actual result targeted css-update is preceded by an earlier full-reload
Expected result no full page reload when a later plugin can handle the stylesheet through CSS HMR

Manual branch-based repro:

git clone https://github.com/benpsnyder/analog.git
cd analog
git checkout fix/tailwind
pnpm install
pnpm exec nx serve tailwind-debug-app

Then edit apps/tailwind-debug-app/src/app/probes/style-probe.component.css and watch the captured HMR traffic in tmp/debug/tailwind-debug-app.vite-ws.log. On the failing path described here, the stylesheet save emits {"type":"full-reload"} before the later css-update payload(s).

Problem Statement

@tailwindcss/vite is forcing a full page reload for an Angular component stylesheet edit even though another plugin later handles the same file with targeted CSS HMR.

Investigation Summary

Signal Finding
Outbound HMR payloads full-reload is emitted before the later CSS HMR updates
Filesystem timing only the component stylesheet changes in the relevant save window
Angular/Analog diagnostics the framework-side stylesheet path reports css-update, not reload fallback
Sender stack the full-reload originates in @tailwindcss/vite

Evidence

App-side Vite wiretap

for (const [environmentName, environment] of Object.entries(server.environments)) {
  const originalHotSend = environment.hot.send.bind(environment.hot)
  environment.hot.send = ((payload: unknown) => {
    const stack =
      typeof payload === 'object' &&
      payload !== null &&
      'type' in payload &&
      (payload as { type?: unknown }).type === 'full-reload'
        ? new Error(`[Adastra ENV HOT] ${environmentName} full-reload`).stack
        : undefined

    writeLog(wsLogPath, {
      environmentName,
      payload,
      source: 'environment.hot.send',
      stack,
    })

    return originalHotSend(payload as never)
  }) as typeof environment.hot.send
}

Captured sender stack

Error: [Adastra ENV HOT] client full-reload
    at Object.send (...vite.config.mts...mjs:101:136)
    at MinimalPluginContext.hotUpdate (.../@tailwindcss/vite/dist/index.mjs:1:3436)
    at handleHMRUpdate (.../vite/dist/node/chunks/node.js:26576:67)
    at onHMRUpdate (.../vite/dist/node/chunks/node.js:26113:41)
    at FSWatcher.<anonymous> (.../vite/dist/node/chunks/node.js:26138:9)

HMR sequence for one stylesheet save

Order Event
1 {"type":"full-reload"} from environment.hot.send
2 browser receives full-reload
3 browser then receives update payload(s) containing css-update

Representative log excerpt:

2026-04-05T00:14:55.220Z {"environmentName":"client","payload":{"type":"full-reload"},"source":"environment.hot.send", ...}
2026-04-05T00:14:55.220Z {"type":"full-reload"}
2026-04-05T00:14:55.227Z {"environmentName":"client","payload":{"type":"update","updates":[{"type":"css-update", ...}]}}
2026-04-05T00:14:55.228Z {"environmentName":"client","payload":{"type":"update","updates":[{"type":"css-update", ...}]}}

File-level evidence

Observation Value
changed file call-to-action.component.css
generated route files in same save window none observed
conclusion the reload is not being triggered by route generation or another generated application file

Relevant Tailwind code path

let isExternalFile =
  modules.length > 0 &&
  modules.every((mod) => mod.type === 'asset' || mod.id === undefined)

if (!isExternalFile) return

if (!isScannedFile(file, modules, roots)) {
  continue
}

if (env === this.environment.name) {
  this.environment.hot.send({ type: 'full-reload' })
} else if (server.hot.send) {
  server.hot.send({ type: 'full-reload' })
} else if (server.ws.send) {
  server.ws.send({ type: 'full-reload' })
}

Why This Looks Upstream

Observation Why it matters
Tailwind treats asset-only / missing-id modules as evidence that no plugin handles the file that assumption is false for this Angular component stylesheet pipeline
Analog later emits a valid css-update for the same file proves the file is still in a handled stylesheet HMR path
Analog diagnostics report outcome: 'css-update' the framework-side branch is not choosing reload as the preferred result

Framework-side references:

Purpose Link
CSS update code path https://github.com/benpsnyder/analog/blob/fix/tailwind/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts#L1146-L1176
preferred css-update / reload fallback logging https://github.com/benpsnyder/analog/blob/fix/tailwind/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts#L3495-L3556
repro breadcrumb utility https://github.com/benpsnyder/analog/blob/fix/tailwind/apps/tailwind-debug-app/src/app/debug/hmr-diagnostics.ts#L29-L186
e2e expectation https://github.com/benpsnyder/analog/blob/fix/tailwind/apps/tailwind-debug-app-e2e/tests/component-css-hmr.spec.ts#L45-L85

Representative Analog diagnostic:

analog:angular:hmr:v component stylesheet hmr outcome {
  file: '.../call-to-action.component.css',
  outcome: 'css-update',
  encapsulation: 'emulated',
  preferredPath: 'css-update',
  pitfalls: ['tracked-wrapper-missing-from-module-graph', 'no-owner-modules']
}

Expected vs Actual

Expected Actual
Angular component stylesheet edit preserve targeted CSS HMR when another plugin can handle it immediate full-reload from @tailwindcss/vite, then later css-update

Likely Fix Direction

Direction Reason
narrow the isExternalFile full-reload branch reserve reloads for true non-module scanned files like HTML / PHP / template changes
avoid forcing full-reload for CSS source files solely because modules are asset-only / missing-id CSS-like files may still be handled by another plugin's stylesheet HMR pipeline

If useful, I can follow up with a minimal public reproduction repo and a proposed patch.

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