Skip to content

Fix compression input file resolution for esproj assets#52283

Merged
javiercn merged 1 commit intomainfrom
fix/esproj-compression-input-file-path
Jan 16, 2026
Merged

Fix compression input file resolution for esproj assets#52283
javiercn merged 1 commit intomainfrom
fix/esproj-compression-input-file-path

Conversation

@javiercn
Copy link
Member

@javiercn javiercn commented Dec 24, 2025

Summary

When compressing static web assets from esproj projects, the compression task incorrectly used the .esproj project file as the input instead of the actual JavaScript asset file. This resulted in compressed files (.gz/.br) containing XML content instead of compressed JavaScript.

Root Cause

The TryFindInputFilePath method in AssetToCompress.cs checked RelatedAssetOriginalItemSpec before RelatedAsset. The esproj SDK has a bug where OriginalItemSpec gets set to the project file path instead of the asset file path (due to referencing %(CandidatePublishAssets.Identity) instead of %(CandidateBuildAssets.Identity) in the GetCurrentProjectBuildStaticWebAssetItems target).

Fix

This PR changes the order of checks in TryFindInputFilePath to prefer RelatedAsset (the asset's Identity path) over RelatedAssetOriginalItemSpec. RelatedAsset contains the correct file path even when OriginalItemSpec is incorrect.

Symptoms this fixes

  • Compressed files (.gz/.br) containing XML content of .esproj file instead of compressed JavaScript
  • JavaScript interop failures in Blazor apps using esproj-based libraries (e.g., Fluent UI)

Testing

Added unit tests to verify:

  • RelatedAsset is preferred when both paths exist
  • Fallback to RelatedAssetOriginalItemSpec when RelatedAsset doesn't exist
  • Error handling when neither path exists
  • Specific esproj scenario with project file vs JS file

All existing tests continue to pass.

#Fixes #50987

Copilot AI review requested due to automatic review settings December 24, 2025 17:31
@github-actions github-actions bot added the Area-AspNetCore RazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDK label Dec 24, 2025
@dotnet-policy-service
Copy link
Contributor

Thanks for your PR, @@javiercn.
To learn about the PR process and branching schedule of this repo, please take a look at the SDK PR Guide.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical bug in the compression of static web assets from esproj projects. Previously, compressed files contained XML content from the .esproj project file instead of the actual JavaScript content because the compression task used the wrong file path.

Key Changes:

  • Reordered file path resolution to prefer RelatedAsset (the asset's Identity) over RelatedAssetOriginalItemSpec (which may incorrectly reference the project file)
  • Added comprehensive unit tests covering the new resolution order and edge cases
  • Updated error message parameter order to match the new check sequence

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/StaticWebAssetsSdk/Tasks/Utils/AssetToCompress.cs Changed TryFindInputFilePath to check RelatedAsset first before falling back to RelatedAssetOriginalItemSpec, and updated error message parameter order to reflect the new sequence
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs Added new test class with 6 comprehensive test cases verifying the corrected file resolution order, fallback behavior, error handling, and the specific esproj scenario

When compressing static web assets from esproj projects, the compression
task incorrectly used the .esproj project file as the input instead of
the actual JavaScript asset file. This happened because the code checked
RelatedAssetOriginalItemSpec before RelatedAsset.

The esproj SDK has a bug where OriginalItemSpec gets set to the project
file path instead of the asset file path (due to referencing
%(CandidatePublishAssets.Identity) instead of %(CandidateBuildAssets.Identity)
in the GetCurrentProjectBuildStaticWebAssetItems target).

This fix changes the order of checks in TryFindInputFilePath to prefer
RelatedAsset (the asset's Identity path) over RelatedAssetOriginalItemSpec.
RelatedAsset contains the correct file path even when OriginalItemSpec
is incorrect.

Symptoms this fixes:
- Compressed files (.gz/.br) containing XML content of .esproj file
- JavaScript interop failures in Blazor apps using esproj-based libraries

Added unit tests to verify:
- RelatedAsset is preferred when both paths exist
- Fallback to RelatedAssetOriginalItemSpec when RelatedAsset doesn't exist
- Error handling when neither path exists
- Specific esproj scenario with project file vs JS file
@javiercn javiercn force-pushed the fix/esproj-compression-input-file-path branch from 702af2d to bf590b9 Compare January 16, 2026 10:12
Copy link
Member

@MackinnonBuck MackinnonBuck left a comment

Choose a reason for hiding this comment

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

Looks good!

Comment on lines +15 to 17
// Check RelatedAsset first (the asset's Identity path) as it's more reliable.
// RelatedAssetOriginalItemSpec may point to a project file (e.g., .esproj) rather than the actual asset.
var relatedAsset = assetToCompress.GetMetadata("RelatedAsset");
Copy link
Member

Choose a reason for hiding this comment

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

Is this more reliable in the general case or only as it relates to the esproj SDK?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it is in general more reliable to first look if the file exists first for the related asset and the only fallback to the original spec if it doesn't. This logic is there because of the trick we have to do for webassembly apps.

@javiercn javiercn merged commit 10972bc into main Jan 16, 2026
26 checks passed
@javiercn javiercn deleted the fix/esproj-compression-input-file-path branch January 16, 2026 20:38
lewing added a commit to dotnet/runtime that referenced this pull request Mar 2, 2026
## Summary

Fix a **.NET 11 regression** causing SRI integrity failures during
incremental Blazor WASM builds. Changes in
`Microsoft.NET.Sdk.WebAssembly.Browser.targets`:

1. **Boot config ContentRoot**: Change the boot config's
`DefineStaticWebAssets` `ContentRoot` from `$(OutDir)wwwroot` to
`$(IntermediateOutputPath)`
2. **Preload matching**: Replace fragile `%(FileName)%(Extension)`-based
scanning with direct references to the boot config output items
3. **WebCil ContentRoot (build-time)**: Use per-item
`ContentRoot="%(RootDir)%(Directory)"` on `_WebCilAssetsCandidates` so
each asset's Identity resolves to its actual file path on disk
4. **WebCil ContentRoot (publish-time)**: Add per-item
`ContentRoot="%(RootDir)%(Directory)"` on both
`_NewWebCilPublishStaticWebAssetsCandidatesNoMetadata` and
`_PromotedWasmPublishStaticWebAssets` — the same fix applied to publish
candidates

## Regression

This is a regression in .NET 11 (works in 10.0). It was introduced by
[dotnet/sdk#52283](dotnet/sdk#52283), which
fixed an esproj compression bug by flipping the order in
`AssetToCompress.TryFindInputFilePath` to prefer `RelatedAsset`
(Identity) over `RelatedAssetOriginalItemSpec`. That fix was correct for
esproj, but exposed a latent issue in the WASM SDK targets: the boot
config and webcil assets' Identity pointed to a `wwwroot` copy rather
than the actual source files.

Before sdk#52283, `OriginalItemSpec` happened to point to the real file
and was checked first, masking the wrong `ContentRoot`. After the flip,
`RelatedAsset` (Identity) is checked first, and its stale `wwwroot` path
is used — producing incorrect SRI hashes on incremental builds.

Reported in
[aspnetcore#65271](dotnet/aspnetcore#65271).

## Problem

The WASM boot config file (e.g. `dotnet.boot.js`) is generated at
`$(IntermediateOutputPath)` (the `obj/` folder), but its static web
asset was defined with `ContentRoot="$(OutDir)wwwroot"`. This caused
`DefineStaticWebAssets` to compute an Identity pointing to the `wwwroot`
copy rather than the actual file in `obj/`.

The same issue applied to WebCil asset candidates — files from
`obj/webcil/`, the runtime pack, and other directories were all defined
with `ContentRoot="$(OutputPath)wwwroot"`, producing synthetic
Identities under `wwwroot/` that could become stale during incremental
builds.

## Fix

### 1. Boot config ContentRoot

Change `ContentRoot` to `$(IntermediateOutputPath)` so the asset
Identity matches the real file location on disk. The
`CopyToOutputDirectory="PreserveNewest"` attribute still ensures the
file is copied to `wwwroot` for serving.

This follows Javier's suggestion in
dotnet/sdk#52847 to "stop defining these assets
with an item spec in the wwwroot folder and just define them in their
original location on disk".

### 2. Preload matching simplification

The `_AddWasmPreloadBuildProperties` and
`_AddWasmPreloadPublishProperties` targets previously scanned all
`@(StaticWebAsset)` items by `%(FileName)%(Extension)` to find the boot
config asset. This relied on the Identity path containing the
fingerprint in the filename, which is an implementation detail of how
`DefineStaticWebAssets` computes Identity based on `ContentRoot`.

The fix replaces the scanning with direct references to
`@(_WasmBuildBootConfigStaticWebAsset)` and
`@(_WasmPublishBootConfigStaticWebAsset)` — the output items already
produced by `DefineStaticWebAssets`. This is both correct and simpler.

### 3. WebCil per-item ContentRoot (build-time)

Instead of using a single task-level `ContentRoot` parameter (which
forces all candidates through the same ContentRoot path, causing
synthesized Identity for files outside that directory), set per-item
`ContentRoot="%(RootDir)%(Directory)"` on each `_WebCilAssetsCandidates`
item. This means:

- **WebCil-converted files** in `obj/webcil/` → ContentRoot = their
parent dir → `FullPath.StartsWith(ContentRoot)` = true → Identity = real
FullPath ✅
- **Runtime pack files** (e.g. `dotnet.native.js`, ICU `.dat`) →
ContentRoot = their parent dir in the runtime pack →
`FullPath.StartsWith(ContentRoot)` = true → Identity = real FullPath ✅

No synthesized paths, no CopyCandidate entries — every asset's Identity
is its actual file on disk.

### 4. WebCil per-item ContentRoot (publish-time)

The publish `DefineStaticWebAssets` call in `ProcessPublishFilesForWasm`
previously had **no ContentRoot at all** — neither task-level nor
per-item. This caused publish candidates (especially promoted build
assets with fingerprint placeholders in their RelativePath) to have
their fingerprinted filename baked into the item spec as Identity,
producing paths like `dotnet.native.7z98fd2ohl.wasm` that don't exist on
disk → MSB3030 "Could not copy file".

The fix adds per-item `ContentRoot="%(RootDir)%(Directory)"` on both:
- `_NewWebCilPublishStaticWebAssetsCandidatesNoMetadata` (freshly
WebCil-converted publish files)
- `_PromotedWasmPublishStaticWebAssets` (build assets promoted to
publish)

**Why this works**: Promoted assets carry `AssetKind=Build` from the
build-time `DefineStaticWebAssets`. In `DefineStaticWebAssets.cs` line
252: `IsPublish("Build") = false`, so contentRoot is NOT nulled for
publish. The per-item ContentRoot = each file's parent directory →
`candidateFullPath.StartsWith(contentRoot)` = true → `computed=false` →
Identity = real FullPath on disk.

## What's not changed

- **Publish boot config ContentRoot**
(`ContentRoot="$(PublishDir)wwwroot"`): Publish builds are clean and
don't have the incremental staleness problem.

## Build Progression

| # | Approach | Build Result | Why |
|---|----------|-------------|-----|
| 1-3 | Boot config + preload only | ✅ Pass | No WebCil changes |
| 4-5 | Per-item ContentRoot (build only) | ❌ Fail | Build works, but
publish has no ContentRoot → MSB3030 |
| 6 | Task-level IntermediateOutputPath | ❌ Fail | Runtime pack files
outside IntermediateOutputPath → synthesized Identity → MSB3030 |
| 7 | **Per-item ContentRoot (build + publish)** | 🔄 Pending | This
commit — applies the fix to both build and publish |

Fixes dotnet/aspnetcore#65271

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Marek Fišera <mara@neptuo.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-AspNetCore RazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StaticWebAssets of esproj: wrong content in compressed files

3 participants