Skip to content

PWA template #18878

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

Merged
merged 30 commits into from
Feb 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dda2211
Add service worker
SteveSandersonMS Feb 7, 2020
79e3ab9
Add manifest
SteveSandersonMS Feb 7, 2020
1820375
Bring back BaselineTest.cs
SteveSandersonMS Feb 7, 2020
43b308e
Add baselines for blazorwasm templates
SteveSandersonMS Feb 7, 2020
251ca60
Add publishing test for PWA template
SteveSandersonMS Feb 7, 2020
176c9be
Baseline fixes
SteveSandersonMS Feb 7, 2020
6b2bc43
Fix baseline test logic to allow for multi-project outputs
SteveSandersonMS Feb 7, 2020
8f0afb2
Remove non-blazorwasm baselines, since this branch now only covers bl…
SteveSandersonMS Feb 7, 2020
a7177ee
Add test for PWA publish output
SteveSandersonMS Feb 7, 2020
ac4f161
Beginning generation of assets manifest
SteveSandersonMS Feb 12, 2020
8fe2b09
Generate assets manifest including blazor outputs
SteveSandersonMS Feb 12, 2020
cd83378
Tweaks
SteveSandersonMS Feb 12, 2020
f34f4c0
Write assets manifest in JSON form
SteveSandersonMS Feb 12, 2020
c7bbf97
Publish service worker
SteveSandersonMS Feb 12, 2020
6cf96c2
Better API
SteveSandersonMS Feb 12, 2020
995704d
More resilience
SteveSandersonMS Feb 12, 2020
1085040
Better API again
SteveSandersonMS Feb 12, 2020
c6537e5
Make ComputeBlazorAssetsManifestItems public as people will need to c…
SteveSandersonMS Feb 12, 2020
68a6224
Exclude service worker files from assets manifest
SteveSandersonMS Feb 12, 2020
35f2579
Use web standard format for hash
SteveSandersonMS Feb 12, 2020
2152288
Update project template
SteveSandersonMS Feb 12, 2020
f198938
In assets manifest, only include items being published
SteveSandersonMS Feb 13, 2020
1db62db
Renames
SteveSandersonMS Feb 13, 2020
accaad8
Compute default assets manifest version by combining hashes
SteveSandersonMS Feb 13, 2020
2850cd0
Emit sw manifest in .js form
SteveSandersonMS Feb 13, 2020
c62d347
Update service worker in project
SteveSandersonMS Feb 13, 2020
cf1ca50
Actually isolate browser instances when requested during E2E tests
SteveSandersonMS Feb 13, 2020
a95170a
E2E test for published PWA operating offline
SteveSandersonMS Feb 13, 2020
bae9941
Fix SWAM path in template
SteveSandersonMS Feb 13, 2020
1be8a95
Clarify targets
SteveSandersonMS Feb 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Blazor.Build
{
public class GenerateServiceWorkerAssetsManifest : Task
{
[Required]
public string Version { get; set; }

[Required]
public ITaskItem[] AssetsWithHashes { get; set; }

[Required]
public string OutputPath { get; set; }

public override bool Execute()
{
using var fileStream = File.Create(OutputPath);
WriteFile(fileStream);
return true;
}

internal void WriteFile(Stream stream)
{
var data = new AssetsManifestFile
{
version = Version,
assets = AssetsWithHashes.Select(item => new AssetsManifestFileEntry
{
url = item.GetMetadata("AssetUrl"),
hash = $"sha256-{item.GetMetadata("FileHash")}",
}).ToArray()
};

using var streamWriter = new StreamWriter(stream, Encoding.UTF8, bufferSize: 50, leaveOpen: true);
streamWriter.Write("self.assetsManifest = ");
streamWriter.Flush();

using var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(stream, Encoding.UTF8, ownsStream: false, indent: true);
new DataContractJsonSerializer(typeof(AssetsManifestFile)).WriteObject(jsonWriter, data);
jsonWriter.Flush();

streamWriter.WriteLine(";");
}

#pragma warning disable IDE1006 // Naming Styles
public class AssetsManifestFile
{
/// <summary>
/// Gets or sets a version string.
/// </summary>
public string version { get; set; }

/// <summary>
/// Gets or sets the assets. Keys are URLs; values are base-64-formatted SHA256 content hashes.
/// </summary>
public AssetsManifestFileEntry[] assets { get; set; }
}

public class AssetsManifestFileEntry
{
/// <summary>
/// Gets or sets the asset URL. Normally this will be relative to the application's base href.
/// </summary>
public string url { get; set; }

/// <summary>
/// Gets or sets the file content hash. This should be the base-64-formatted SHA256 value.
/// </summary>
public string hash { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles
}
}
1 change: 1 addition & 0 deletions src/Components/Blazor/Build/src/targets/All.targets
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<Import Project="Blazor.MonoRuntime.targets" />
<Import Project="Publish.targets" />
<Import Project="StaticWebAssets.targets" />
<Import Project="ServiceWorkerAssetsManifest.targets" />

<Target Name="GenerateBlazorMetadataFile"
BeforeTargets="GetCopyToOutputDirectoryItems">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

<Target
Name="_BlazorCopyFilesToOutputDirectory"
DependsOnTargets="PrepareBlazorOutputs"
DependsOnTargets="PrepareBlazorOutputs;$(_BlazorCopyFilesToOutputDirectoryDependsOn)"
AfterTargets="CopyFilesToOutputDirectory"
Condition="'$(OutputType.ToLowerInvariant())'=='exe'">

Expand Down Expand Up @@ -133,7 +133,7 @@
ReferenceCopyLocalPaths includes all files that are part of the build out with CopyLocalLockFileAssemblies on.
Remove assemblies that are inputs to calculating the assembly closure. Instead use the resolved outputs, since it is the minimal set.
-->
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" />
<_BlazorCopyLocalPaths Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.dll'" />
<_BlazorCopyLocalPaths Remove="@(_BlazorManagedRuntimeAssemby)" />

<BlazorOutputWithTargetPath Include="@(_BlazorCopyLocalPaths)">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<Project>

<PropertyGroup>
<_BlazorCopyFilesToOutputDirectoryDependsOn>
$(_BlazorCopyFilesToOutputDirectoryDependsOn);
_ComputeServiceWorkerAssetsManifestInputs;
_WriteServiceWorkerAssetsManifest;
</_BlazorCopyFilesToOutputDirectoryDependsOn>
</PropertyGroup>

<Target Name="_ComputeServiceWorkerAssetsManifestInputs"
Condition="'$(ServiceWorkerAssetsManifest)' != ''"
DependsOnTargets="PrepareBlazorOutputs">

<PropertyGroup>
<_ServiceWorkerAssetsManifestIntermediateOutputPath>$(BlazorIntermediateOutputPath)serviceworkerassets.js</_ServiceWorkerAssetsManifestIntermediateOutputPath>
</PropertyGroup>

<ItemGroup>
<!-- Include _framework/* content -->
<ServiceWorkerAssetsManifestItem
Include="@(BlazorOutputWithTargetPath)"
Condition="$([System.String]::Copy('%(BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').StartsWith('dist/'))">
<AssetUrl>$([System.String]::Copy('%(BlazorOutputWithTargetPath.TargetOutputPath)').Replace('\','/').Substring(5))</AssetUrl>
</ServiceWorkerAssetsManifestItem>

<!-- Include content from wwwroot -->
<ServiceWorkerAssetsManifestItem
Include="@(ContentWithTargetPath)"
Condition="
('%(ContentWithTargetPath.CopyToPublishDirectory)' == 'Always' OR '%(ContentWithTargetPath.CopyToPublishDirectory)' == 'PreserveNewest')
AND $([System.String]::Copy('%(ContentWithTargetPath.TargetPath)').Replace('\','/').StartsWith('wwwroot/'))">
<AssetUrl>$([System.String]::Copy('%(ContentWithTargetPath.TargetPath)').Replace('\','/').Substring(8))</AssetUrl>
</ServiceWorkerAssetsManifestItem>

<!-- Include SWA from references -->
<ServiceWorkerAssetsManifestItem
Include="@(StaticWebAsset)"
Condition="'%(StaticWebAsset.SourceType)' != ''">
<AssetUrl>%(StaticWebAsset.BasePath)/%(StaticWebAsset.RelativePath)</AssetUrl>
</ServiceWorkerAssetsManifestItem>
</ItemGroup>

</Target>

<UsingTask TaskName="GenerateServiceWorkerAssetsManifest" AssemblyFile="$(BlazorTasksPath)" />

<Target Name="_WriteServiceWorkerAssetsManifest"
Inputs="@(ServiceWorkerAssetsManifestItem)"
Outputs="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
DependsOnTargets="_ComputeServiceWorkerAssetsManifestFileHashes; _ComputeDefaultServiceWorkerAssetsManifestVersion">

<GenerateServiceWorkerAssetsManifest
Version="$(ServiceWorkerAssetsManifestVersion)"
AssetsWithHashes="@(_ServiceWorkerAssetsManifestItemWithHash)"
OutputPath="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />

<ItemGroup>
<BlazorOutputWithTargetPath
Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)"
TargetOutputPath="$(BaseBlazorDistPath)$(ServiceWorkerAssetsManifest)" />

<FileWrites Include="$(_ServiceWorkerAssetsManifestIntermediateOutputPath)" />
</ItemGroup>

</Target>

<Target Name="_ComputeServiceWorkerAssetsManifestFileHashes">
<GetFileHash Files="@(ServiceWorkerAssetsManifestItem)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestItemWithHash" />
</GetFileHash>
</Target>

<!--
If no ServiceWorkerAssetsManifestVersion was specified, we compute a default 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)' == ''">
<PropertyGroup>
<_CombinedHashIntermediatePath>$(BlazorIntermediateOutputPath)serviceworkerhashes.txt</_CombinedHashIntermediatePath>
</PropertyGroup>

<WriteLinesToFile
File="$(_CombinedHashIntermediatePath)"
Lines="@(_ServiceWorkerAssetsManifestItemWithHash->'%(FileHash)')"
Overwrite="true" />

<GetFileHash Files="$(_CombinedHashIntermediatePath)" Algorithm="SHA256" HashEncoding="base64">
<Output TaskParameter="Items" ItemName="_ServiceWorkerAssetsManifestCombinedHash" />
</GetFileHash>

<PropertyGroup>
<ServiceWorkerAssetsManifestVersion>$([System.String]::Copy('%(_ServiceWorkerAssetsManifestCombinedHash.FileHash)').Substring(0, 8))</ServiceWorkerAssetsManifestVersion>
</PropertyGroup>
</Target>

</Project>
2 changes: 1 addition & 1 deletion src/Components/startvs.cmd
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
@ECHO OFF

%~dp0..\..\startvs.cmd %~dp0Components.sln
%~dp0..\..\startvs.cmd %~dp0Blazor.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<RazorLangVersion>3.0</RazorLangVersion>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>

<ItemGroup>
Expand All @@ -11,10 +12,22 @@
<PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="${MicrosoftAspNetCoreBlazorDevServerPackageVersion}" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Blazor.HttpClient" Version="${MicrosoftAspNetCoreBlazorHttpClientPackageVersion}" />
</ItemGroup>

<!--#if Hosted -->
<ItemGroup>
<ProjectReference Include="..\Shared\BlazorWasm-CSharp.Shared.csproj" />
</ItemGroup>

<!--#endif -->
<!--#if PWA -->
<ItemGroup>
<!-- When publishing, swap service-worker.published.js in place of service-worker.js -->
<Content Update="wwwroot\service-worker*.js" CopyToPublishDirectory="false" />
<ContentWithTargetPath Include="wwwroot\service-worker.published.js">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<TargetPath>wwwroot\service-worker.js</TargetPath>
</ContentWithTargetPath>
</ItemGroup>

<!--#endif -->
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
"longName": "no-restore",
"shortName": ""
},
"Hosted": {
"longName": "hosted"
},
"Hosted": {
"longName": "hosted"
},
"PWA": {
"longName": "pwa"
},
"Framework": {
"longName": "framework"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@
"exclude": [
"*.sln"
]
},
{
"condition": "(!PWA)",
"exclude": [
"Client/wwwroot/service-worker*.js",
"Client/wwwroot/manifest.json",
"Client/wwwroot/icon-512.png"
]
}
]
}
Expand Down Expand Up @@ -147,6 +155,12 @@
"fallbackVariableName": "HttpsPortGenerated"
},
"replaces": "44300"
},
"PWA": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"description": "If specified, produces a Progressive Web Application (PWA) supporting installation and offline use."
}
},
"tags": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
"text": "ASP.NET Core _hosted"
},
"isVisible": "true"
},
{
"id": "PWA",
"name": {
"text": "_Progressive Web Application"
},
"isVisible": "true"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" />
<!--#if PWA -->
<link href="manifest.json" rel="manifest" />
<!--#endif -->
</head>

<body>
Expand All @@ -19,6 +22,9 @@
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<!--#if PWA -->
<script>navigator.serviceWorker.register('service-worker.js');</script>
<!--#endif -->
</body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "BlazorWasm-CSharp",
"short_name": "BlazorWasm-CSharp",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations

self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));

const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];

async function onInstall(event) {
console.info('Service worker: Install');

// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}

async function onActivate(event) {
console.info('Service worker: Activate');

// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}

async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate';

const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}

return cachedResponse || fetch(event.request);
}
Loading