Skip to content

TailwindMvc/TailwindRazor's Tailwind target 404s in production if wwwroot/css/site.css isn't committed (dotnet/aspnetcore#68641) #1

Description

@Popl7

Summary

Both TailwindMvc and TailwindRazor use the same shape for the Tailwind MSBuild target:

<Target Name="Tailwind" BeforeTargets="Build">
  <Exec ... Command="npm install" Condition=" !Exists('Client/node_modules') " />
  <Exec Command="npx @tailwindcss/cli -i ./css/site.css -o ../wwwroot/css/site.css" WorkingDirectory="Client" />
</Target>

This writes straight into wwwroot/css/site.css from a target hooked at BeforeTargets="Build", with no explicit <Content Include=".." Link="wwwroot/.." /> item. Under .NET 9/10's MapStaticAssets(), that's a known, reproducible bug: dotnet/aspnetcore#68641. The generated CSS route can silently go missing from the build-time static web assets manifest even though the file itself is written to disk correctly, and GET /css/site.css 404s in production.

Why your repo doesn't currently hit this

wwwroot/css/site.css is checked into git in this repo (not gitignored) — a leftover, already-built file from a previous run. Because it's already present on disk before MSBuild evaluates the project, the SDK's wwwroot/** default-item glob discovers it and registers its manifest route at evaluation time, before the Tailwind target ever runs. The target then just overwrites that same physical file's contents in place — nothing new appears mid-build, so there's nothing to race. That's presumably accidental (most people gitignore generated build output), not a deliberate workaround.

Reproduction

Forked and tested both projects, unmodified except for adding a Dockerfile + CI to check the built manifest and actual HTTP response:

Suggestion

Two independent ways to make this actually safe rather than accidentally safe:

  1. Explicitly document that wwwroot/css/site.css must be committed (not gitignored) for this exact reason — a one-line note would prevent readers from hitting this the moment they "clean up" their repo.
  2. Or adopt the pattern from Microsoft's own guidance for hooking generated assets into static web assets — write to $(IntermediateOutputPath) and register an explicit <Content Include=".." Link="wwwroot/.." /> item instead of writing straight into wwwroot: https://devblogs.microsoft.com/dotnet/build-client-web-assets-for-your-razor-class-library/. That's safe regardless of whether the output is committed, and is the fix that's held up in every test across Custom BeforeBuild target's generated wwwroot file missing from staticwebassets.endpoints.json depending on build environment dotnet/aspnetcore#68641.

Happy to open a PR for either if useful.

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