Write hash to service worker on publish (#20131)

* Write SWAM version into service worker output

* Update project template

* Add publishing test

* Update src/Components/WebAssembly/Build/src/targets/ServiceWorkerAssetsManifest.targets

Co-Authored-By: Pranav K <prkrishn@hotmail.com>

* Add E2E test for hosted PWA too

* Avoid test clashes

* E2E test fix

* E2E test fix

* E2E test fix

Co-authored-by: Pranav K <prkrishn@hotmail.com>
This commit is contained in:
Steve Sanderson 2020-03-25 20:06:03 +00:00 committed by GitHub
parent b9d7772816
commit bbe9cca327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 28 deletions

View File

@ -44,7 +44,12 @@
Condition="'%(RelativePath)' != '$(ServiceWorkerAssetsManifest)'">
<AssetUrl>$([System.String]::Copy('$([System.String]::Copy('%(StaticWebAsset.BasePath)').TrimEnd('/'))/%(StaticWebAsset.RelativePath)').Replace('\','/').TrimStart('/'))</AssetUrl>
</ServiceWorkerAssetsManifestItem>
<!-- Don't include compressed files in the manifest, since their existence is transparent to the client -->
<ServiceWorkerAssetsManifestItem Remove="@(_CompressedStaticWebAsset->'%(FullPath)')" />
<!-- Don't include the service worker files in the manifest, as the service worker doesn't need to fetch itself -->
<ServiceWorkerAssetsManifestItem Remove="%(_ServiceWorkerIntermediateFile.FullPath)" />
</ItemGroup>
<GetFileHash Files="@(ServiceWorkerAssetsManifestItem)" Algorithm="SHA256" HashEncoding="base64">
@ -53,11 +58,10 @@
</Target>
<!--
If no ServiceWorkerAssetsManifestVersion was specified, we compute a default value by combining all the asset hashes.
Compute a default ServiceWorkerAssetsManifestVersion value by combining all the asset hashes.
This is useful because then clients will only have to repopulate caches if the contents have changed.
-->
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion"
Condition="'$(ServiceWorkerAssetsManifestVersion)' == ''">
<Target Name="_ComputeDefaultServiceWorkerAssetsManifestVersion" Condition="'$(ServiceWorkerAssetsManifest)' != ''">
<PropertyGroup>
<_CombinedHashIntermediatePath>$(_BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
</PropertyGroup>
@ -65,6 +69,7 @@
<WriteLinesToFile
File="$(_CombinedHashIntermediatePath)"
Lines="@(_ServiceWorkerAssetsManifestItemWithHash->'%(FileHash)')"
WriteOnlyWhenDifferent="true"
Overwrite="true" />
<GetFileHash Files="$(_CombinedHashIntermediatePath)" Algorithm="SHA256" HashEncoding="base64">
@ -72,8 +77,50 @@
</GetFileHash>
<PropertyGroup>
<ServiceWorkerAssetsManifestVersion>$([System.String]::Copy('%(_ServiceWorkerAssetsManifestCombinedHash.FileHash)').Substring(0, 8))</ServiceWorkerAssetsManifestVersion>
<ServiceWorkerAssetsManifestVersion Condition="'$(ServiceWorkerAssetsManifestVersion)' == ''">$([System.String]::Copy('%(_ServiceWorkerAssetsManifestCombinedHash.FileHash)').Substring(0, 8))</ServiceWorkerAssetsManifestVersion>
</PropertyGroup>
</Target>
<Target Name="_OmitServiceWorkerContent" BeforeTargets="AssignTargetPaths">
<ItemGroup>
<!-- Don't emit the service worker source files to the output -->
<Content Remove="@(ServiceWorker)" />
<Content Remove="@(ServiceWorker->'%(PublishedContent)')" />
</ItemGroup>
</Target>
<Target Name="_ResolveServiceWorkerOutputs"
BeforeTargets="_ResolveBlazorOutputs"
DependsOnTargets="_ComputeServiceWorkerOutputs; _GenerateServiceWorkerIntermediateFiles">
<ItemGroup>
<_BlazorOutputWithTargetPath Include="@(_ServiceWorkerIntermediateFile)" />
</ItemGroup>
</Target>
<Target Name="_ComputeServiceWorkerOutputs">
<ItemGroup>
<!-- Figure out where we're getting the content for each @(ServiceWorker) entry, depending on whether there's a PublishedContent value -->
<_ServiceWorkerIntermediateFile Include="@(ServiceWorker->'$(IntermediateOutputPath)blazor\serviceworkers\%(Identity)')">
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' != ''">%(ServiceWorker.PublishedContent)</ContentSourcePath>
<ContentSourcePath Condition="'%(ServiceWorker.PublishedContent)' == ''">%(ServiceWorker.Identity)</ContentSourcePath>
<TargetOutputPath>%(ServiceWorker.Identity)</TargetOutputPath>
<TargetOutputPath Condition="$([System.String]::Copy('%(ServiceWorker.Identity)').StartsWith('wwwroot\'))">$([System.String]::Copy('%(ServiceWorker.Identity)').Substring(8))</TargetOutputPath>
</_ServiceWorkerIntermediateFile>
</ItemGroup>
</Target>
<Target Name="_GenerateServiceWorkerIntermediateFiles"
Inputs="@(_ServiceWorkerIntermediateFile->'%(ContentSourcePath)'); $(_CombinedHashIntermediatePath)"
Outputs="@(_ServiceWorkerIntermediateFile)"
DependsOnTargets="_ComputeDefaultServiceWorkerAssetsManifestVersion">
<Copy SourceFiles="%(_ServiceWorkerIntermediateFile.ContentSourcePath)" DestinationFiles="%(_ServiceWorkerIntermediateFile.Identity)" />
<WriteLinesToFile
File="%(_ServiceWorkerIntermediateFile.Identity)"
Lines="/* Manifest version: $(ServiceWorkerAssetsManifestVersion) */"
Condition="'$(ServiceWorkerAssetsManifestVersion)' != ''" />
<ItemGroup>
<FileWrites Include="%(_ServiceWorkerIntermediateFile.Identity)" />
</ItemGroup>
</Target>
</Project>

View File

@ -16,6 +16,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="${MicrosoftAspNetCoreComponentsWebAssemblyAuthenticationPackageVersion}" Condition="'$(IndividualLocalAuth)' == 'true'" />
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="${MicrosoftAuthenticationWebAssemblyMsalPackageVersion}" Condition="'$(OrganizationalAuth)' == 'true' OR '$(IndividualB2CAuth)' == 'true'" />
</ItemGroup>
<!--#if Hosted -->
<ItemGroup>
<ProjectReference Include="..\Shared\ComponentsWebAssembly-CSharp.Shared.csproj" />
@ -23,10 +24,8 @@
<!--#endif -->
<!--#if PWA -->
<!-- When publishing, swap service-worker.published.js in place of service-worker.js -->
<ItemGroup Condition="'$(DesignTimeBuild)' != 'true'">
<Content Remove="wwwroot\service-worker.js" />
<Content Update="wwwroot\service-worker.published.js" Link="wwwroot\service-worker.js" />
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
<!--#endif -->

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.E2ETesting;
@ -95,9 +96,9 @@ namespace Templates.Test
}
[Fact]
public async Task BlazorWasmPwaTemplate_Works()
public async Task BlazorWasmStandalonePwaTemplate_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorpwa", Output);
var project = await ProjectFactory.GetOrCreateProject("blazorstandalonepwa", Output);
project.TargetFramework = "netstandard2.1";
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--pwa" });
@ -111,13 +112,7 @@ namespace Templates.Test
await BuildAndRunTest(project.ProjectName, project);
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
// When publishing the PWA template, we generate an assets manifest
// and move service-worker.published.js to overwrite service-worker.js
Assert.False(File.Exists(Path.Combine(publishDir, "service-worker.published.js")), "service-worker.published.js should not be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker.js")), "service-worker.js should be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "service-worker-assets.js should be published");
ValidatePublishedServiceWorker(project);
using (var serverProcess = RunPublishedStandaloneBlazorProject(project))
{
@ -129,15 +124,86 @@ namespace Templates.Test
// The PWA template supports offline use. By now, the browser should have cached everything it needs,
// so we can continue working even without the server.
ValidateAppWorksOffline(project, listeningUri);
ValidateAppWorksOffline(project, listeningUri, skipFetchData: false);
}
private void ValidateAppWorksOffline(Project project, string listeningUri)
[Fact]
public async Task BlazorWasmHostedPwaTemplate_Works()
{
var project = await ProjectFactory.GetOrCreateProject("blazorhostedpwa", Output);
var createResult = await project.RunDotNetNewAsync("blazorwasm", args: new[] { "--hosted", "--pwa" });
Assert.True(0 == createResult.ExitCode, ErrorMessages.GetFailedProcessMessage("create/restore", project, createResult));
var serverProject = GetSubProject(project, "Server", $"{project.ProjectName}.Server");
var publishResult = await serverProject.RunDotNetPublishAsync();
Assert.True(0 == publishResult.ExitCode, ErrorMessages.GetFailedProcessMessage("publish", serverProject, publishResult));
var buildResult = await serverProject.RunDotNetBuildAsync();
Assert.True(0 == buildResult.ExitCode, ErrorMessages.GetFailedProcessMessage("build", serverProject, buildResult));
await BuildAndRunTest(project.ProjectName, serverProject);
ValidatePublishedServiceWorker(serverProject);
string listeningUri;
using (var aspNetProcess = serverProject.StartPublishedProjectAsync())
{
Assert.False(
aspNetProcess.Process.HasExited,
ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", serverProject, aspNetProcess.Process));
await aspNetProcess.AssertStatusCode("/", HttpStatusCode.OK, "text/html");
if (BrowserFixture.IsHostAutomationSupported())
{
aspNetProcess.VisitInBrowser(Browser);
TestBasicNavigation(project.ProjectName);
}
else
{
BrowserFixture.EnforceSupportedConfigurations();
}
// Note: we don't want to use aspNetProcess.ListeningUri because that isn't necessarily the HTTPS URI
var browserUri = new Uri(Browser.Url);
listeningUri = $"{browserUri.Scheme}://{browserUri.Authority}";
}
// The PWA template supports offline use. By now, the browser should have cached everything it needs,
// so we can continue working even without the server.
// Since this is the hosted project, backend APIs won't work offline, so we need to skip "fetchdata"
ValidateAppWorksOffline(project, listeningUri, skipFetchData: true);
}
private void ValidatePublishedServiceWorker(Project project)
{
var publishDir = Path.Combine(project.TemplatePublishDir, "wwwroot");
// When publishing the PWA template, we generate an assets manifest
// and move service-worker.published.js to overwrite service-worker.js
Assert.False(File.Exists(Path.Combine(publishDir, "service-worker.published.js")), "service-worker.published.js should not be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker.js")), "service-worker.js should be published");
Assert.True(File.Exists(Path.Combine(publishDir, "service-worker-assets.js")), "service-worker-assets.js should be published");
// We automatically append the SWAM version as a comment in the published service worker file
var serviceWorkerAssetsManifestContents = ReadFile(publishDir, "service-worker-assets.js");
var serviceWorkerContents = ReadFile(publishDir, "service-worker.js");
// Parse the "version": "..." value from the SWAM, and check it's in the service worker
var serviceWorkerAssetsManifestVersionMatch = new Regex(@"^\s*\""version\"":\s*\""([^\""]+)\""", RegexOptions.Multiline)
.Match(serviceWorkerAssetsManifestContents);
Assert.True(serviceWorkerAssetsManifestVersionMatch.Success);
var serviceWorkerAssetsManifestVersion = serviceWorkerAssetsManifestVersionMatch.Groups[1].Captures[0];
Assert.True(serviceWorkerContents.Contains($"/* Manifest version: {serviceWorkerAssetsManifestVersion} */", StringComparison.Ordinal));
}
private void ValidateAppWorksOffline(Project project, string listeningUri, bool skipFetchData)
{
Browser.Navigate().GoToUrl("about:blank"); // Be sure we're really reloading
Output.WriteLine($"Opening browser without corresponding server at {listeningUri}...");
Browser.Navigate().GoToUrl(listeningUri);
TestBasicNavigation(project.ProjectName);
TestBasicNavigation(project.ProjectName, skipFetchData: skipFetchData);
}
[Theory]
@ -333,7 +399,7 @@ namespace Templates.Test
}
}
private void TestBasicNavigation(string appName, bool usesAuth = false)
private void TestBasicNavigation(string appName, bool usesAuth = false, bool skipFetchData = false)
{
// Start fresh always
if (usesAuth)
@ -399,14 +465,17 @@ namespace Templates.Test
Browser.Equal(appName.Trim(), () => Browser.Title.Trim());
}
// Can navigate to the 'fetch data' page
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
Browser.Contains("fetchdata", () => Browser.Url);
Browser.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
if (!skipFetchData)
{
// Can navigate to the 'fetch data' page
Browser.FindElement(By.PartialLinkText("Fetch data")).Click();
Browser.Contains("fetchdata", () => Browser.Url);
Browser.Equal("Weather forecast", () => Browser.FindElement(By.TagName("h1")).Text);
// Asynchronously loads and displays the table of weather forecasts
Browser.Exists(By.CssSelector("table>tbody>tr"));
Browser.Equal(5, () => Browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
// Asynchronously loads and displays the table of weather forecasts
Browser.Exists(By.CssSelector("table>tbody>tr"));
Browser.Equal(5, () => Browser.FindElements(By.CssSelector("p+table>tbody>tr")).Count);
}
}
private string ReadFile(string basePath, string path)