Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

<PageTitle>Home</PageTitle>

<h1>222</h1>
<h1>111</h1>

<h1>Hello, world!</h1>

Expand Down
4 changes: 3 additions & 1 deletion src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

self.assetsExclude = [/\.scp\.css$/, /weather\.json$/];
self.caseInsensitiveUrl = true;
self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/];

self.externalAssets = [
{
"url": "not-found/script.file.js"
}
];
// 'lax' opts into best-effort installs: the demo intentionally references a non-existent
// asset to exercise the progress / error reporting UI. Under the default 'strict' setting
// that would abort the install. See README.md > errorTolerance.
self.errorTolerance = 'lax';

self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js');
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

self.assetsExclude = [/\.scp\.css$/, /weather\.json$/];
self.caseInsensitiveUrl = true;
self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/];

//self.externalAssets = [
// {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
</head>

<body>
<h1>111</h1>
<Routes @rendermode="@renderMode" />
<script src="_framework/blazor.web.js?v=10.0.0" autostart="false"></script>
<script src="_content/Bit.Bswup/bit-bswup.js"
Expand Down
124 changes: 98 additions & 26 deletions src/Bswup/Bit.Bswup/Bit.Bswup.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
<ResolveStaticWebAssetsInputsDependsOn Condition="'$(TargetFramework)' == 'net10.0'">
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>

<ItemGroup>
Expand All @@ -21,41 +17,117 @@
<PackageReference Condition="'$(TargetFramework)' == 'net10.0'" Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0" />
</ItemGroup>

<!-- Keep the build inputs (tsconfig + package manifests) out of the published static web
assets while still showing them in the project. -->
<ItemGroup>
<Content Remove="tsconfig.json" />
<None Include="tsconfig.json" />
<Content Remove="package*.json;tsconfig*.json" />
<None Include="package*.json;tsconfig*.json" />
</ItemGroup>

<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<!-- Restores npm packages, compiles the TypeScript bundles and CSS, then (in Release) minifies
the JS bundles. The sub-targets are incremental via their Inputs/Outputs; a full
regenerate-from-source on each `dotnet build`/`pack` is forced by
BuildBswupAssetsOnCrossTargeting deleting the outputs first (see its comment). -->
<Target Name="BuildBswupAssets" DependsOnTargets="InstallNodejsDependencies;BuildJavaScript;MinifyAssets;BuildCss" />

<Target Name="BeforeBuildTasks" AfterTargets="CoreCompile" Condition="'$(TargetFramework)' == 'net10.0'">
<CallTarget Targets="InstallNodejsDependencies"/>
<CallTarget Targets="BuildJavaScript"/>
<CallTarget Targets="BuildCss"/>
<!-- The bundles (wwwroot/bit-bswup*.js and wwwroot/bit-bswup.progress.css) are generated,
git-ignored files. On a clean build they do not exist when the default "wwwroot/**"
Content glob is evaluated, so the static web asset discovery in
ResolveProjectStaticWebAssets would miss them and consumers get a 404 for
_content/Bit.Bswup/bit-bswup.js (this is what breaks the demo on a fresh checkout).
ResolveProjectStaticWebAssets runs BeforeTargets="AssignTargetPaths" and does NOT depend
on ResolveStaticWebAssetsInputs, so the old ResolveStaticWebAssetsInputsDependsOn hook
never ran early enough. Hooking BeforeTargets="ResolveProjectStaticWebAssets" guarantees
the bundles are generated first (DependsOnTargets) and then explicitly added to
@(Content) so discovery picks them up. The Remove-before-Include keeps each to a single
Content entry (avoiding a duplicate / conflicting-target-path error) for builds where the
glob already captured them. -->
<Target Name="IncludeBswupAssetsAsStaticWebAsset"
BeforeTargets="ResolveProjectStaticWebAssets"
DependsOnTargets="BuildBswupAssets">
<ItemGroup>
<Content Remove="wwwroot\bit-bswup.js" />
<Content Include="wwwroot\bit-bswup.js" />
<Content Remove="wwwroot\bit-bswup.progress.js" />
<Content Include="wwwroot\bit-bswup.progress.js" />
<Content Remove="wwwroot\bit-bswup.sw.js" />
<Content Include="wwwroot\bit-bswup.sw.js" />
<Content Remove="wwwroot\bit-bswup.sw-cleanup.js" />
<Content Include="wwwroot\bit-bswup.sw-cleanup.js" />
<Content Remove="wwwroot\bit-bswup.progress.css" />
<Content Include="wwwroot\bit-bswup.progress.css" />
</ItemGroup>
</Target>

<Target Name="InstallNodejsDependencies" Inputs="package.json" Outputs="node_modules\.package-lock.json">
<!-- Force a full regenerate-from-source in the single outer (cross-targeting) build, which is
what `dotnet build` / `dotnet pack` (no -f) use. Removing the generated outputs (and the
minify stamp) first means the incremental sub-targets cannot treat them as "up to date",
so tsc/esbuild always re-emit the bundles from the TypeScript/CSS sources. This is what
makes a build reliably overwrite whatever currently sits in wwwroot - stale, unminified,
or manually edited content alike. The regeneration runs exactly once here, before the
parallel inner per-TFM builds start; those then see the fresh outputs and skip (the
sub-targets are incremental), which avoids the esbuild/Move race across inner builds. -->
<Target Name="BuildBswupAssetsOnCrossTargeting"
BeforeTargets="DispatchToInnerBuilds"
Condition="'$(IsCrossTargetingBuild)' == 'true'">
<Delete Files="wwwroot\bit-bswup.js;wwwroot\bit-bswup.progress.js;wwwroot\bit-bswup.sw.js;wwwroot\bit-bswup.sw-cleanup.js;wwwroot\bit-bswup.progress.css;$(BaseIntermediateOutputPath)$(Configuration)\bit-bswup.min.stamp" />
<CallTarget Targets="BuildBswupAssets" />
</Target>

<Target Name="InstallNodejsDependencies" Inputs="package.json;package-lock.json" Outputs="node_modules\.package-lock.json">
<Exec Command="npm install" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>

<Target Name="BuildJavaScript" Inputs="Scripts/bit-bswup.ts;Scripts/bit-bswup.progress.ts;Scripts/bit-bswup.sw.ts" Outputs="wwwroot/bit-bswup.js;wwwroot/bit-bswup.progress.js;wwwroot/bit-bswup.sw.js">
<!-- Compiles the TypeScript bundles. Incremental on the .ts/tsconfig inputs vs the emitted
wwwroot .js outputs, so a no-op when nothing changed. This always emits the unminified
bundles; Release minification is handled separately by MinifyAssets. -->
<Target Name="BuildJavaScript" Inputs="Scripts/bit-bswup.ts;Scripts/bit-bswup.progress.ts;Scripts/bit-bswup.sw.ts;Scripts/bit-bswup.sw-cleanup.ts;tsconfig.json;tsconfig.sw.json;package.json;package-lock.json" Outputs="wwwroot/bit-bswup.js;wwwroot/bit-bswup.progress.js;wwwroot/bit-bswup.sw.js;wwwroot/bit-bswup.sw-cleanup.js">
<!-- Page scripts (DOM lib) and service-worker scripts (WebWorker lib) are compiled by
separate tsconfigs because the DOM and WebWorker type libraries cannot coexist in
one compilation. Both emit into wwwroot. -->
<Exec Command="node_modules/.bin/tsc" StandardOutputImportance="high" StandardErrorImportance="high" />
<Exec Condition=" '$(Configuration)' == 'Release' " Command="node_modules/.bin/esbuild wwwroot/bit-bswup.js --minify --outfile=wwwroot/bit-bswup.js --allow-overwrite" StandardOutputImportance="high" StandardErrorImportance="high" />
<Exec Condition=" '$(Configuration)' == 'Release' " Command="node_modules/.bin/esbuild wwwroot/bit-bswup.progress.js --minify --outfile=wwwroot/bit-bswup.progress.js --allow-overwrite" StandardOutputImportance="high" StandardErrorImportance="high" />
<Exec Condition=" '$(Configuration)' == 'Release' " Command="node_modules/.bin/esbuild wwwroot/bit-bswup.sw.js --minify --outfile=wwwroot/bit-bswup.sw.js --allow-overwrite" StandardOutputImportance="high" StandardErrorImportance="high" />
<Exec Command="node_modules/.bin/tsc -p tsconfig.sw.json" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>

<!-- Release-only minification of the JS bundles. Incremental, but deliberately NOT keyed off
a configuration stamp the way tsc is: it is keyed off the emitted wwwroot bundles (the
tsc output) versus a single config-specific, TFM-independent stamp.

Why this exact shape:
- Debug and Release share the same wwwroot output files. A Debug build emits UNMINIFIED
bundles; when it does, those files' timestamps advance past this stamp, so the next
Release build re-minifies them (fixing the "Release didn't overwrite the unminified
files" bug). A plain config stamp could not catch this because a stamp from a prior
Release build survives an intervening Debug build.
- The stamp is TFM-independent ($(BaseIntermediateOutputPath)$(Configuration)\, i.e.
obj\<Config>\) so that in a cross-targeting build the outer build minifies once and
stamps, and the parallel inner per-TFM builds then see this target up to date and SKIP.
Without that, the inner builds would all run esbuild against the same shared wwwroot
files at once and race on the temp/Move (MSB3680). The Move (temp -> final) also avoids
leaving a half-written bundle if an in-place minify were interrupted. -->
<Target Name="MinifyAssets"
Condition="'$(Configuration)' == 'Release'"
DependsOnTargets="BuildJavaScript"
Inputs="wwwroot/bit-bswup.js;wwwroot/bit-bswup.progress.js;wwwroot/bit-bswup.sw.js;wwwroot/bit-bswup.sw-cleanup.js"
Outputs="$(BaseIntermediateOutputPath)$(Configuration)\bit-bswup.min.stamp">
<!-- The outer cross-targeting build never compiles, so MSBuild has not created its
intermediate directory; create it before Touch writes the stamp there. -->
<MakeDir Directories="$(BaseIntermediateOutputPath)$(Configuration)" />
<Exec Command="node_modules/.bin/esbuild wwwroot/bit-bswup.js --minify --outfile=wwwroot/bit-bswup.min.js" StandardOutputImportance="high" StandardErrorImportance="high" />
<Move SourceFiles="wwwroot/bit-bswup.min.js" DestinationFiles="wwwroot/bit-bswup.js" OverwriteReadOnlyFiles="true" />
<Exec Command="node_modules/.bin/esbuild wwwroot/bit-bswup.progress.js --minify --outfile=wwwroot/bit-bswup.progress.min.js" StandardOutputImportance="high" StandardErrorImportance="high" />
<Move SourceFiles="wwwroot/bit-bswup.progress.min.js" DestinationFiles="wwwroot/bit-bswup.progress.js" OverwriteReadOnlyFiles="true" />
<Exec Command="node_modules/.bin/esbuild wwwroot/bit-bswup.sw.js --minify --outfile=wwwroot/bit-bswup.sw.min.js" StandardOutputImportance="high" StandardErrorImportance="high" />
<Move SourceFiles="wwwroot/bit-bswup.sw.min.js" DestinationFiles="wwwroot/bit-bswup.sw.js" OverwriteReadOnlyFiles="true" />
<Exec Command="node_modules/.bin/esbuild wwwroot/bit-bswup.sw-cleanup.js --minify --outfile=wwwroot/bit-bswup.sw-cleanup.min.js" StandardOutputImportance="high" StandardErrorImportance="high" />
<Move SourceFiles="wwwroot/bit-bswup.sw-cleanup.min.js" DestinationFiles="wwwroot/bit-bswup.sw-cleanup.js" OverwriteReadOnlyFiles="true" />
<!-- Touched last so the stamp is always newer than the (re-minified) bundles, keeping the
target up to date until tsc next rewrites them. -->
<Touch Files="$(BaseIntermediateOutputPath)$(Configuration)\bit-bswup.min.stamp" AlwaysCreate="true" />
</Target>

<Target Name="BuildCss" Inputs="Styles/bit-bswup.progress.css" Outputs="wwwroot/bit-bswup.progress.css">
<Exec Command="node_modules/.bin/esbuild Styles/bit-bswup.progress.css --minify --outfile=wwwroot/bit-bswup.progress.css" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>

<ItemGroup>
<Content Remove="package*.json" />
<Content Remove="tsconfig.json" />
<None Include="package*json" />
<None Include="tsconfig.json" />
</ItemGroup>

</Project>
</Project>
42 changes: 36 additions & 6 deletions src/Bswup/Bit.Bswup/BswupProgress.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,54 @@
[Parameter] public string? Handler { get; set; }
}

<div id="bit-bswup">
@if (ChildContent is not null)
@* Configuration is published as data-* attributes and read by bit-bswup.progress.js when it
loads (it self-initializes from these attributes). This deliberately avoids emitting an
inline <script>:
- a strict Content-Security-Policy (e.g. script-src 'self') would block an inline
script - the same CSP the README recommends for hardening the service worker; and
- inline <script> tags added by an interactive Blazor renderer are not executed by the
browser, so the old approach only worked when this component was part of the
statically-rendered host document.
Razor attribute-encodes these values and the script reads them back via getAttribute, so
no value can break out of the attribute into markup or script. Make sure
_content/Bit.Bswup/bit-bswup.progress.js is referenced on the page. *@
<div id="bit-bswup"
style="display: none;"
data-bit-bswup-config="true"
data-bit-bswup-auto-reload="@(AutoReload ? "true" : "false")"
data-bit-bswup-show-logs="@(ShowLogs ? "true" : "false")"
data-bit-bswup-show-assets="@(ShowAssets ? "true" : "false")"
data-bit-bswup-app-container="@AppContainer"
data-bit-bswup-hide-app="@(HideApp ? "true" : "false")"
data-bit-bswup-auto-hide="@(AutoHide ? "true" : "false")"
data-bit-bswup-handler="@Handler">
@if (ChildContent is not null)
{
@ChildContent
}
else
else
{
<div class="bit-bswup-container">
<p class="bit-bswup-title">New version is available</p>
<p class="bit-bswup-description">Downloading updates, please wait...</p>
<div class="bit-bswup-progress">
<div id="bit-bswup-progress-bar" style="width: 0%"></div>
<div id="bit-bswup-progress-bar"
role="progressbar"
aria-label="Update download progress"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
style="width: 0%"></div>
</div>
<p id="bit-bswup-percent">0 %</p>
<ul id="bit-bswup-assets" style="display: @(ShowAssets ? "block" : "none");"></ul>
<div id="bit-bswup-error" class="bit-bswup-error" style="display: none;" role="alert">
<p class="bit-bswup-error-title">Update failed to install</p>
<p id="bit-bswup-error-message" class="bit-bswup-error-message"></p>
<pre id="bit-bswup-error-details" class="bit-bswup-error-details"></pre>
<button id="bit-bswup-error-retry" type="button">Retry</button>
</div>
</div>
<button id="bit-bswup-reload">Update ready to install!</button>
}
<img style="display: none" src=""
onerror="BitBswupProgress.start(@(AutoReload ? "true" : "false"), @(ShowLogs ? "true" : "false"), @(ShowAssets ? "true" : "false"), '@(AppContainer)', @(HideApp ? "true" : "false"), @(AutoHide ? "true" : "false"), '@(Handler)')">
</div>
Loading
Loading