Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write import map to html files #46233

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open

Conversation

maraf
Copy link
Member

@maraf maraf commented Jan 23, 2025

Rewrites html files to include import map and put fingerprint into referenced assets

  • Placeholder for import map is <script type="importmap"></script>
  • Placeholder for referenced assets is "main#[{.fingerprint}].js". Unfortunately root scripts event with type=module are not affected by import map and inline script with import has other disadvantages. So we need a syntax for the rewrite. This syntax is the same as can be used in server process razor files.

Enabled by WriteImportMapToHtml=true. At the moment the feature is opt-in, but I believe we should make it opt-out, possibly later in the cycle if we gather any feedback.

For Blazor scenarios it solves two issues

  • Staleness of dotnet.js by writing the import map into the index.html file. This issue more pressing as runtime scripts has fixed interface between them
  • Staleness of blazor.webassembly.js by adding the fingerprint to the referenced script.

Open questions

  • Should we produce a warning/error when no HTML files was overriden?
  • Should the feature be opt-out for Blazor? If so, we need to modify the blazorwasm template

Sample input & output for wasmbrowser based index.html

Input

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="main.css">
  <script type="importmap"></script>
  <script type="module" src="main#[{.fingerprint}].js"></script>
</head>

<body>
</body>

</html>

Output

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="main.css">
  <script type='importmap'>{
  "imports": {
    "./_framework/dotnet.native.js": "./_framework/dotnet.native.hmanx9razn.js",
    "./_framework/dotnet.js": "./_framework/dotnet.nseotsck93.js",
    "./_framework/dotnet.runtime.js": "./_framework/dotnet.runtime.o8gq1i8bk6.js",
    "./main.js": "./main.mfy8p4tgv8.js"
  },
  "integrity": {
    "./_framework/dotnet.js": "sha256-GbKWK+esqeL1E24rCVsCvsQCzaJM65aknWOT1UhUYa4=",
    "./_framework/dotnet.native.hmanx9razn.js": "sha256-d7UNSArplVrX3arY2Kmhxp1wN79ng8hLqIBrkiQ9Gbo=",
    "./_framework/dotnet.native.js": "sha256-d7UNSArplVrX3arY2Kmhxp1wN79ng8hLqIBrkiQ9Gbo=",
    "./_framework/dotnet.nseotsck93.js": "sha256-GbKWK+esqeL1E24rCVsCvsQCzaJM65aknWOT1UhUYa4=",
    "./_framework/dotnet.runtime.js": "sha256-uD1t4tsPtmIHsx30SC4OztehGGaHVDksFD38rL2e3P4=",
    "./_framework/dotnet.runtime.o8gq1i8bk6.js": "sha256-uD1t4tsPtmIHsx30SC4OztehGGaHVDksFD38rL2e3P4=",
    "./main.js": "sha256-robmb5E/1hOMs8P+gA+pFJTEGIgj3Sdhzhdv3UAzV8Y=",
    "./main.mfy8p4tgv8.js": "sha256-robmb5E/1hOMs8P+gA+pFJTEGIgj3Sdhzhdv3UAzV8Y="
  }
}</script>
  <script type="module" src="main.mfy8p4tgv8.js"></script>
</head>

<body>
</body>

</html>

@maraf maraf self-assigned this Jan 23, 2025
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged Request triage from a team member label Jan 23, 2025
@javiercn
Copy link
Member

I would settle on one syntax. Rather than using the HTML comment, I would detect <script type="importmap"></script> or <script type="importmap" /> (the second one is invalid syntax, which might be good to detect errors when the file hasn't been processed.

<PropertyGroup>
<ResolveBuildRelatedStaticWebAssetsDependsOn>
$(ResolveStaticWebAssetsInputsDependsOn);
_UpdateHtmlFiles;
Copy link
Member

Choose a reason for hiding this comment

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

Note: This needs to happen also before the service-worker-manifest gets generated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Why is that? The html documents are not part of service worker assets manifest, aren't they?

Copy link
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

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

Overall the direction looks good. The main pieces of feedback are:

  • Settle on a single syntax. I would avoid using @Assets as it might set the wrong expectations.
    • Maybe just adding #[.{fingerprint}] which is something we can interpret and recognize.
  • I would avoid blanket fingerprinting all the JS assets, and only limit things to stuff we know about we'll get right.
  • I would avoid all the copy/paste from the runtime and work directly with the list of endpoints to generate the importmap.

The main missing piece here is also the publish bit, which needs to be accounted for. It'll be good if we follow the same template as https://github.com/dotnet/sdk/blob/main/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.JSModules.targets where there is

  • Resolve(Feature)(Build|Publish)Configuration:
    • Figures out what assets/files we need to consider
  • Generate(Feature)(Build|Publish)StaticWebAssets:
    • Does the work of generating any outputs.
  • Resolve(Feature(Build|Publish)StaticWebAssets.
    • Defines the assets and endpoints for the outputs.

@maraf
Copy link
Member Author

maraf commented Feb 3, 2025

About the syntax

  1. Using invalid <script type="importmap" /> results in broken syntax highlighting in VS/code.
    image
    Currently using empty import map <script type="importmap"></script>

  2. Asset path replacement needs to be something that has a clean start and end boundaries. Using just main#[.{fingerprint}]!.js would be impossible to match

@javiercn
Copy link
Member

javiercn commented Feb 3, 2025

Asset path replacement needs to be something that has a clean start and end boundaries. Using just main#[.{fingerprint}]!.js would be impossible to match

I was thinking in something like `="(.#[.].*)" would be enough, wouldn't it? Then look at $1.

@maraf
Copy link
Member Author

maraf commented Feb 3, 2025

I had the same idea later, originally thought it might be limiting to just html attribute, but maybe that's actually good. I'll update the PR to that pattern

@maraf
Copy link
Member Author

maraf commented Feb 5, 2025

Publish test will fail until the runtime PR is resolved

@maraf maraf requested a review from javiercn February 6, 2025 09:44
@maraf maraf marked this pull request as ready for review February 9, 2025 11:42
@maraf maraf requested review from a team as code owners February 9, 2025 11:42

<PropertyGroup>

<!--
Copy link
Member

Choose a reason for hiding this comment

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

This comment is explaining the execution order ? Maybe you can clarify that

<MakeDir Directories="$(_BuildImportMapHtmlPath)"/>

<ItemGroup>
<_HtmlStaticWebAssets Include="@(StaticWebAsset)" Condition="'%(AssetKind)' != 'Publish' and '%(Extension)' == '.html'" />
Copy link
Member

Choose a reason for hiding this comment

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

Should we also do older .htm ?

OutputPath="$(_BuildImportMapHtmlPath)">
<Output TaskParameter="HtmlCandidates" ItemName="_HtmlCandidates" />
<Output TaskParameter="HtmlFilesToRemove" ItemName="_HtmlFilesToRemove" />
<Output TaskParameter="FileWrites" ItemName="FileWrites" />
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 friendly to incremental build ?

if (asset.Label != null)
{
imports ??= [];
imports[$"./{asset.Label}"] = $"./{asset.Url}";
Copy link
Member

Choose a reason for hiding this comment

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

Can asset label contain some interesting characters ? Does the URL need URL encoding ?

@@ -44,4 +45,53 @@ public void Build_FingerprintsContent_WhenEnabled()
AssertManifest(manifest1, expectedManifest);
AssertBuildAssets(manifest1, outputPath, intermediateOutputPath);
}

Copy link
Member

@javiercn javiercn Feb 11, 2025

Choose a reason for hiding this comment

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

Can we add a test that covers Blazor too? (Hosted and Standalone) or is that covered elsewhere?

Comment on lines +25 to +59
<ResolveBuildRelatedStaticWebAssetsDependsOn>
$(ResolveBuildRelatedStaticWebAssetsDependsOn);
ResolveHtmlImportMapBuildStaticWebAssets;
</ResolveBuildRelatedStaticWebAssetsDependsOn>
<ResolveCompressedFilesDependsOn>
$(ResolveCompressedFilesDependsOn);
ResolveHtmlImportMapBuildStaticWebAssets
</ResolveCompressedFilesDependsOn>
<ResolveHtmlImportMapBuildStaticWebAssetsDependsOn>
GenerateHtmlImportMapBuildStaticWebAssets;
$(ResolveHtmlImportMapBuildStaticWebAssetsDependsOn)
</ResolveHtmlImportMapBuildStaticWebAssetsDependsOn>
<GenerateHtmlImportMapBuildStaticWebAssetsDependsOn>
ResolveHtmlImportMapBuildConfiguration;
$(GenerateHtmlImportMapBuildStaticWebAssetsDependsOn)
</GenerateHtmlImportMapBuildStaticWebAssetsDependsOn>

<!--
ResolvePublishRelatedStaticWebAssets
ResolveHtmlImportMapPublishStaticWebAssets
GenerateHtmlImportMapPublishStaticWebAssets
ResolveHtmlImportMapPublishConfiguration
-->
<ResolvePublishRelatedStaticWebAssetsDependsOn>
$(ResolvePublishRelatedStaticWebAssetsDependsOn);
ResolveHtmlImportMapPublishStaticWebAssets
</ResolvePublishRelatedStaticWebAssetsDependsOn>
<ResolveHtmlImportMapPublishStaticWebAssetsDependsOn>
GenerateHtmlImportMapPublishStaticWebAssets;
$(ResolveHtmlImportMapPublishStaticWebAssetsDependsOn)
</ResolveHtmlImportMapPublishStaticWebAssetsDependsOn>
<GenerateHtmlImportMapPublishStaticWebAssetsDependsOn>
ResolveHtmlImportMapPublishConfiguration;
$(GenerateHtmlImportMapPublishStaticWebAssetsDependsOn)
</GenerateHtmlImportMapPublishStaticWebAssetsDependsOn>
Copy link
Member

Choose a reason for hiding this comment

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

We need to ensure that this runs before the service worker is generated.

Copy link
Member

Choose a reason for hiding this comment

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

Also needs to run before compression happens

<ItemGroup>
<StaticWebAsset Remove="@(_HtmlFilesToRemove)" />
<StaticWebAsset Include="@(_UpdatedHtmlStaticWebAssets)" />
<StaticWebAssetEndpoint Include="@(_UpdatedHtmlStaticWebAssetsEndpoint)" />
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we remove the old endpoints too?

Copy link
Member

@javiercn javiercn 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, but I think there are a few additional things we should solve before getting it merged.


<Target Name="ResolveHtmlImportMapBuildConfiguration">
<PropertyGroup>
<_BuildImportMapHtmlPath>$([MSBuild]::NormalizeDirectory($(IntermediateOutputPath), 'importmaphtml', 'build'))</_BuildImportMapHtmlPath>
Copy link
Member

Choose a reason for hiding this comment

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


if (content != outputContent)
{
string outputPath = Path.Combine(OutputPath, Path.GetRandomFileName() + item.GetMetadata("Extension"));
Copy link
Member

Choose a reason for hiding this comment

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

Don't do this. Use a hash of the content if anything, but don't use Random as this makes things non-deterministic

Copy link
Member

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-WasmSdk untriaged Request triage from a team member
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants