Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 42 additions & 15 deletions src/Butil/Bit.Butil/Bit.Butil.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks>
<IsTrimmable>true</IsTrimmable>
<ResolveStaticWebAssetsInputsDependsOn Condition="'$(TargetFramework)' == 'net10.0'">
BeforeBuildTasks;
$(ResolveStaticWebAssetsInputsDependsOn)
</ResolveStaticWebAssetsInputsDependsOn>
<!-- IL2026 is suppressed for THIS assembly's own build only; it does not affect consumers.
The JSON-(de)serializing public APIs (e.g. ButilStorage.GetItem<T>/SetItem<T>,
BroadcastChannel.Post<T>, ServiceWorker.PostMessage<T>, History.GetState<T>) are each
individually annotated with [RequiresUnreferencedCode]/[RequiresDynamicCode], so a
trimming/AOT consumer still gets the warning at their call site. The warnings produced
here are just the internal propagation of those (already-annotated) members through the
synchronous fast-invoke helpers, which only ever pass trim-safe primitives. Keeping the
real protection (the attributes) while quieting the redundant internal noise. -->
<NoWarn>$(NoWarn);IL2026</NoWarn>
</PropertyGroup>

Expand All @@ -28,14 +32,44 @@
</ItemGroup>

<ItemGroup>
<TypeScriptFiles Include="**\*.ts" />
<TypeScriptFiles Include="Scripts\**\*.ts" />
</ItemGroup>

<Target Name="BeforeBuildTasks" AfterTargets="CoreCompile" Condition="'$(TargetFramework)' == 'net10.0'">
<CallTarget Targets="InstallNodejsDependencies" />
<CallTarget Targets="BuildJavaScript" />
<!-- Restores npm packages then compiles the TypeScript bundle. Both sub-targets are incremental
via their Inputs/Outputs, so this is a no-op when nothing changed. -->
<Target Name="BuildButilJavaScript" DependsOnTargets="InstallNodejsDependencies;BuildJavaScript" />

<!-- The bundle (wwwroot/bit-butil.js) is a generated, git-ignored file. On a clean build it does
not exist when the default "wwwroot/**" Content glob is evaluated, so the static web asset
discovery in ResolveProjectStaticWebAssets would miss it and consumers get a 404 for
_content/Bit.Butil/bit-butil.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 bundle is
generated first (DependsOnTargets) and then explicitly added to @(Content) so discovery picks
it up. The Remove-before-Include keeps it to a single Content entry (avoiding a duplicate /
conflicting-target-path error) for builds where the glob already captured it. -->
<Target Name="IncludeButilJavaScriptAsStaticWebAsset"
BeforeTargets="ResolveProjectStaticWebAssets"
DependsOnTargets="BuildButilJavaScript">
<ItemGroup>
<Content Remove="wwwroot\bit-butil.js" />
<Content Include="wwwroot\bit-butil.js" />
</ItemGroup>
</Target>

<!-- In a multi-targeted build, produce the bundle exactly once in the outer (cross-targeting)
build, before the per-TFM inner builds are dispatched. This prevents several inner builds
from invoking tsc/esbuild concurrently against the same shared wwwroot output file.
The Condition is evaluated at execution time (when IsCrossTargetingBuild is set), so the
target only runs in the outer build; inner builds emit a harmless "DispatchToInnerBuilds
does not exist" message at detailed verbosity only. -->
<Target Name="BuildButilJavaScriptOnCrossTargeting"
BeforeTargets="DispatchToInnerBuilds"
Condition="'$(IsCrossTargetingBuild)' == 'true'"
DependsOnTargets="BuildButilJavaScript" />

<Target Name="InstallNodejsDependencies" Inputs="package.json" Outputs="node_modules\.package-lock.json">
<Exec Command="npm install" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>
Expand All @@ -45,11 +79,4 @@
<Exec Condition=" '$(Configuration)' == 'Release' " Command="node_modules/.bin/esbuild wwwroot/bit-butil.js --minify --outfile=wwwroot/bit-butil.js --allow-overwrite" StandardOutputImportance="high" StandardErrorImportance="high" />
</Target>

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

</Project>
82 changes: 62 additions & 20 deletions src/Butil/Bit.Butil/BitButil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,82 @@ public static class BitButil
{
public static IServiceCollection AddBitButilServices(this IServiceCollection services)
{
services.AddTransient<Clipboard>();
services.AddTransient<Console>();
services.AddTransient<Cookie>();
services.AddTransient<Crypto>();
services.AddTransient<Document>();
services.AddTransient<History>();
services.AddTransient<Keyboard>();
services.AddTransient<LocalStorage>();
services.AddTransient<SessionStorage>();
services.AddTransient<Location>();
services.AddTransient<Navigator>();
services.AddTransient<Notification>();
services.AddTransient<Screen>();
services.AddTransient<ScreenOrientation>();
services.AddTransient<UserAgent>();
services.AddTransient<VisualViewport>();
services.AddTransient<Window>();
services.AddTransient<WebAuthn>();
// Scoped matches Blazor's "one circuit / one WASM app instance per user" model.
// Transient would create a fresh wrapper on every @inject, fragmenting per-instance
// listener bookkeeping and keeping captured component delegates alive longer than
// the component itself.
services.AddScoped<Clipboard>();
services.AddScoped<Console>();
services.AddScoped<Cookie>();
services.AddScoped<CookieStore>();
services.AddScoped<Crypto>();
services.AddScoped<Battery>();
services.AddScoped<BackgroundSync>();
services.AddScoped<BroadcastChannel>();
services.AddScoped<CacheStorage>();
services.AddScoped<ContactPicker>();
services.AddScoped<Document>();
services.AddScoped<EyeDropper>();
services.AddScoped<Fetch>();
services.AddScoped<FileReader>();
services.AddScoped<Geolocation>();
services.AddScoped<History>();
services.AddScoped<IdleDetector>();
services.AddScoped<IndexedDb>();
services.AddScoped<Keyboard>();
services.AddScoped<LocalStorage>();
services.AddScoped<SessionStorage>();
services.AddScoped<Location>();
services.AddScoped<MediaDevices>();
services.AddScoped<Navigator>();
services.AddScoped<NetworkInformation>();
services.AddScoped<Nfc>();
services.AddScoped<Notification>();
services.AddScoped<ObjectUrls>();
services.AddScoped<Performance>();
services.AddScoped<Permissions>();
services.AddScoped<Push>();
services.AddScoped<Reporting>();
services.AddScoped<Screen>();
services.AddScoped<ScreenOrientation>();
services.AddScoped<ServiceWorker>();
services.AddScoped<SpeechRecognition>();
services.AddScoped<SpeechSynthesis>();
services.AddScoped<StorageManager>();
services.AddScoped<UserAgent>();
services.AddScoped<VisualViewport>();
services.AddScoped<WakeLock>();
services.AddScoped<WebAudio>();
services.AddScoped<WebLocks>();
services.AddScoped<Window>();
services.AddScoped<WebAuthn>();
Comment thread
msynk marked this conversation as resolved.

return services;
}

internal static bool FastInvokeEnabled { get; private set; }

/// <summary>
/// Enables the use of the fast APIs globally when available (Invoke methods of IJSInProcessRuntime).
/// Enables the synchronous in-process ("fast") invoke path for the APIs that opt into it.
/// <br/>
/// Only APIs backed by synchronous JavaScript functions (for example <see cref="LocalStorage"/>,
/// <see cref="SessionStorage"/>, <see cref="Cookie"/>, <see cref="Console"/> and <see cref="Location"/>)
/// use this path; everything that wraps an asynchronous (Promise-returning) browser API always runs
/// asynchronously regardless of this setting, so enabling it can't break those calls.
/// Only effective on Blazor WebAssembly (where an <see cref="Microsoft.JSInterop.IJSInProcessRuntime"/> is available).
/// <br/>
/// NOTE: this is a process-wide static toggle, not per-app/per-circuit. It is intended to be set
/// once at startup. On Blazor Server it is effectively a no-op (the fast path always falls back to
/// the async path because there is no in-process runtime), so sharing it across circuits is benign.
/// </summary>
public static void UseFastInvoke()
{
FastInvokeEnabled = true;
}

/// <summary>
/// Disables the use of the fast APIs globally when available (Invoke methods of IJSInProcessRuntime).
/// Disables the synchronous in-process ("fast") invoke path; all calls run asynchronously.
/// Process-wide static toggle — see <see cref="UseFastInvoke"/>.
/// </summary>
public static void UseNormalInvoke()
{
Expand Down
Loading
Loading