Fix compression input file resolution for esproj assets#52283
Conversation
|
Thanks for your PR, @@javiercn. |
There was a problem hiding this comment.
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) overRelatedAssetOriginalItemSpec(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
702af2d to
bf590b9
Compare
| // 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"); |
There was a problem hiding this comment.
Is this more reliable in the general case or only as it relates to the esproj SDK?
There was a problem hiding this comment.
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.
## 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>
Summary
When compressing static web assets from esproj projects, the compression task incorrectly used the
.esprojproject 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
TryFindInputFilePathmethod inAssetToCompress.cscheckedRelatedAssetOriginalItemSpecbeforeRelatedAsset. The esproj SDK has a bug whereOriginalItemSpecgets set to the project file path instead of the asset file path (due to referencing%(CandidatePublishAssets.Identity)instead of%(CandidateBuildAssets.Identity)in theGetCurrentProjectBuildStaticWebAssetItemstarget).Fix
This PR changes the order of checks in
TryFindInputFilePathto preferRelatedAsset(the asset's Identity path) overRelatedAssetOriginalItemSpec.RelatedAssetcontains the correct file path even whenOriginalItemSpecis incorrect.Symptoms this fixes
.gz/.br) containing XML content of.esprojfile instead of compressed JavaScriptTesting
Added unit tests to verify:
RelatedAssetis preferred when both paths existRelatedAssetOriginalItemSpecwhenRelatedAssetdoesn't existAll existing tests continue to pass.
#Fixes #50987