Browse Source

Merge pull request #8820 from AvaloniaUI/typescript-bundling

pull/8911/head
Dan Walmsley 4 years ago
committed by GitHub
parent
commit
283f7430ab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 33
      src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj
  3. 16
      src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs
  4. 18
      src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs
  5. 41
      src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs
  6. 31
      src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs
  7. 12
      src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs
  8. 32
      src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs
  9. 45
      src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs
  10. 41
      src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs
  11. 2
      src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs
  12. 41
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts
  13. 23
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts
  14. 261
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts
  15. 68
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts
  16. 7
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts
  17. 326
      src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts
  18. 14
      src/Web/Avalonia.Web.Blazor/tsconfig.json
  19. 5
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts
  20. 40
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts
  21. 22
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts
  22. 35
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts
  23. 255
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts
  24. 67
      src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts
  25. 79
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts
  26. 172
      src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts
  27. 16
      src/Web/Avalonia.Web.Blazor/webapp/package.json
  28. 18
      src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json
  29. 0
      src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts
  30. 40
      src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js

3
.gitignore

@ -212,3 +212,6 @@ coc-settings.json
*.map
src/Web/Avalonia.Web.Blazor/wwwroot/*.js
src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js
node_modules
src/Web/Avalonia.Web.Blazor/webapp/package-lock.json
src/Web/Avalonia.Web.Blazor/wwwroot

33
src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj

@ -8,31 +8,16 @@
<MSBuildEnableWorkloadResolver>false</MSBuildEnableWorkloadResolver>
<StaticWebAssetsDisableProjectBuildPropsFileGeneration>true</StaticWebAssetsDisableProjectBuildPropsFileGeneration>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser" />
</ItemGroup>
<PropertyGroup>
<TypescriptOutDir>wwwroot</TypescriptOutDir>
<TypeScriptNoEmitOnError>true</TypeScriptNoEmitOnError>
<TypeScriptNoImplicitReturns>true</TypeScriptNoImplicitReturns>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<TypeScriptRemoveComments>false</TypeScriptRemoveComments>
<TypeScriptSourceMap>true</TypeScriptSourceMap>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TypeScriptRemoveComments>true</TypeScriptRemoveComments>
<TypeScriptSourceMap>false</TypeScriptSourceMap>
</PropertyGroup>
<Import Project="..\..\..\build\BuildTargets.targets" />
<Import Project="..\..\..\build\SkiaSharp.props" />
<Import Project="..\..\..\build\HarfBuzzSharp.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<ItemGroup>
<Content Include="*.props">
<Pack>true</Pack>
@ -47,10 +32,24 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.8" />
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.7.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<Target Name="NpmInstall" Inputs="webapp/package.json" Outputs="webapp/node_modules/.install-stamp">
<Exec Command="npm install" WorkingDirectory="webapp" />
<!-- Write the stamp file, so incremental builds work -->
<Touch Files="webapp/node_modules/.install-stamp" AlwaysCreate="true" />
</Target>
<Target Name="NpmRunBuild" DependsOnTargets="NpmInstall" BeforeTargets="BeforeBuild">
<Exec Command="npm run build" WorkingDirectory="webapp" />
</Target>
</Project>

16
src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs

@ -28,6 +28,7 @@ namespace Avalonia.Web.Blazor
private SizeWatcherInterop? _sizeWatcher = null;
private DpiWatcherInterop? _dpiWatcher = null;
private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null;
private AvaloniaModule? _avaloniaModule = null;
private InputHelperInterop? _inputHelper = null;
private InputHelperInterop? _canvasHelper = null;
private NativeControlHostInterop? _nativeControlHost = null;
@ -241,8 +242,10 @@ namespace Avalonia.Web.Blazor
{
AvaloniaLocator.CurrentMutable.Bind<IJSInProcessRuntime>().ToConstant((IJSInProcessRuntime)Js);
_inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement);
_canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas);
_avaloniaModule = await AvaloniaModule.ImportAsync(Js);
_inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement);
_canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas);
_inputHelper.Hide();
_canvasHelper.SetCursor("default");
@ -252,11 +255,11 @@ namespace Avalonia.Web.Blazor
_canvasHelper.SetCursor(x); //windows
};
_nativeControlHost = await NativeControlHostInterop.ImportAsync(Js, _nativeControlsContainer);
_nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer);
_storageProvider = await StorageProviderInterop.ImportAsync(Js);
Console.WriteLine("starting html canvas setup");
_interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame);
_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame);
Console.WriteLine("Interop created");
@ -306,9 +309,10 @@ namespace Avalonia.Web.Blazor
{
_interop.RequestAnimationFrame(true);
_sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged);
_dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged);
_sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged);
_dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged);
_sizeWatcher.Start();
_topLevel.Prepare();
_topLevel.Renderer.Start();

18
src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs

@ -0,0 +1,18 @@
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class AvaloniaModule : JSModuleInterop
{
private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/avalonia.js")
{
}
public static async Task<AvaloniaModule> ImportAsync(IJSRuntime js)
{
var interop = new AvaloniaModule(js);
await interop.ImportAsync();
return interop;
}
}
}

41
src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs

@ -1,43 +1,29 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class DpiWatcherInterop : JSModuleInterop
internal class DpiWatcherInterop : IDisposable
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/DpiWatcher.js";
private const string StartSymbol = "DpiWatcher.start";
private const string StopSymbol = "DpiWatcher.stop";
private const string GetDpiSymbol = "DpiWatcher.getDpi";
private static DpiWatcherInterop? instance;
private event Action<double>? callbacksEvent;
private readonly FloatFloatActionHelper callbackHelper;
private readonly FloatFloatActionHelper _callbackHelper;
private readonly AvaloniaModule _module;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
public static async Task<DpiWatcherInterop> ImportAsync(IJSRuntime js, Action<double>? callback = null)
public DpiWatcherInterop(AvaloniaModule module, Action<double>? callback = null)
{
var interop = Get(js);
await interop.ImportAsync();
if (callback != null)
interop.Subscribe(callback);
return interop;
}
_module = module;
_callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
public static DpiWatcherInterop Get(IJSRuntime js) =>
instance ??= new DpiWatcherInterop(js);
private DpiWatcherInterop(IJSRuntime js)
: base(js, JsFilename)
{
callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n));
if (callback != null)
Subscribe(callback);
}
protected override void OnDisposingModule() =>
Stop();
public void Dispose() => Stop();
public void Subscribe(Action<double> callback)
{
@ -65,9 +51,9 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference != null)
return GetDpi();
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return Invoke<double>(StartSymbol, callbackReference);
return _module.Invoke<double>(StartSymbol, callbackReference);
}
private void Stop()
@ -75,13 +61,12 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference == null)
return;
Invoke(StopSymbol);
_module.Invoke(StopSymbol);
callbackReference?.Dispose();
callbackReference = null;
}
public double GetDpi() =>
Invoke<double>(GetDpiSymbol);
public double GetDpi() => _module.Invoke<double>(GetDpiSymbol);
}
}

31
src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs

@ -1,41 +1,32 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class InputHelperInterop : JSModuleInterop
internal class InputHelperInterop
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js";
private const string ClearSymbol = "InputHelper.clear";
private const string FocusSymbol = "InputHelper.focus";
private const string SetCursorSymbol = "InputHelper.setCursor";
private const string HideSymbol = "InputHelper.hide";
private const string ShowSymbol = "InputHelper.show";
private readonly ElementReference inputElement;
private readonly AvaloniaModule _module;
private readonly ElementReference _inputElement;
public static async Task<InputHelperInterop> ImportAsync(IJSRuntime js, ElementReference element)
public InputHelperInterop(AvaloniaModule module, ElementReference element)
{
var interop = new InputHelperInterop(js, element);
await interop.ImportAsync();
return interop;
_module = module;
_inputElement = element;
}
public InputHelperInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
inputElement = element;
}
public void Clear() => Invoke(ClearSymbol, inputElement);
public void Clear() => _module.Invoke(ClearSymbol, _inputElement);
public void Focus() => Invoke(FocusSymbol, inputElement);
public void Focus() => _module.Invoke(FocusSymbol, _inputElement);
public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind);
public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind);
public void Hide() => Invoke(HideSymbol, inputElement);
public void Hide() => _module.Invoke(HideSymbol, _inputElement);
public void Show() => Invoke(ShowSymbol, inputElement);
public void Show() => _module.Invoke(ShowSymbol, _inputElement);
}
}

12
src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs

@ -1,6 +1,4 @@
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
@ -31,16 +29,16 @@ namespace Avalonia.Web.Blazor.Interop
protected IJSUnmarshalledObjectReference Module =>
module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first.");
protected void Invoke(string identifier, params object?[]? args) =>
internal void Invoke(string identifier, params object?[]? args) =>
Module.InvokeVoid(identifier, args);
protected TValue Invoke<TValue>(string identifier, params object?[]? args) =>
internal TValue Invoke<TValue>(string identifier, params object?[]? args) =>
Module.Invoke<TValue>(identifier, args);
protected ValueTask InvokeAsync(string identifier, params object?[]? args) =>
internal ValueTask InvokeAsync(string identifier, params object?[]? args) =>
Module.InvokeVoidAsync(identifier, args);
protected ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
internal ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object?[]? args) =>
Module.InvokeAsync<TValue>(identifier, args);
protected virtual void OnDisposingModule() { }

32
src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs

@ -1,5 +1,4 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
@ -10,31 +9,24 @@ using Microsoft.JSInterop;
namespace Avalonia.Web.Blazor.Interop
{
internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl
internal class NativeControlHostInterop : INativeControlHostImpl
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js";
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild";
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment";
private const string GetReferenceSymbol = "NativeControlHost.GetReference";
private readonly ElementReference hostElement;
private readonly AvaloniaModule _module;
private readonly ElementReference _hostElement;
public static async Task<NativeControlHostInterop> ImportAsync(IJSRuntime js, ElementReference element)
public NativeControlHostInterop(AvaloniaModule module, ElementReference element)
{
var interop = new NativeControlHostInterop(js, element);
await interop.ImportAsync();
return interop;
}
public NativeControlHostInterop(IJSRuntime js, ElementReference element)
: base(js, JsFilename)
{
hostElement = element;
_module = module;
_hostElement = element;
}
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent)
{
var element = Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
var element = _module.Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol);
return new JSObjectControlHandle(element);
}
@ -43,9 +35,9 @@ namespace Avalonia.Web.Blazor.Interop
Attachment? a = null;
try
{
using var hostElementJsReference = Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, hostElement);
using var hostElementJsReference = _module.Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, _hostElement);
var child = create(new JSObjectControlHandle(hostElementJsReference));
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
// It has to be assigned to the variable before property setter is called so we dispose it on exception
#pragma warning disable IDE0017 // Simplify object initialization
a = new Attachment(attachmenetReference, child);
@ -62,7 +54,7 @@ namespace Avalonia.Web.Blazor.Interop
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle)
{
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var attachmenetReference = _module.Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol);
var a = new Attachment(attachmenetReference, handle);
a.AttachedTo = this;
return a;
@ -111,7 +103,7 @@ namespace Avalonia.Web.Blazor.Interop
}
else
{
_native.InvokeVoid(AttachToSymbol, host.hostElement);
_native.InvokeVoid(AttachToSymbol, host._hostElement);
}
_attachedTo = host;
}

45
src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs

@ -4,7 +4,7 @@ using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SKHtmlCanvasInterop : JSModuleInterop
internal class SKHtmlCanvasInterop : IDisposable
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js";
private const string InitGLSymbol = "SKHtmlCanvas.initGL";
@ -14,39 +14,32 @@ namespace Avalonia.Web.Blazor.Interop
private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize";
private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData";
private readonly ElementReference htmlCanvas;
private readonly string htmlElementId;
private readonly ActionHelper callbackHelper;
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlCanvas;
private readonly string _htmlElementId;
private readonly ActionHelper _callbackHelper;
private DotNetObjectReference<ActionHelper>? callbackReference;
public static async Task<SKHtmlCanvasInterop> ImportAsync(IJSRuntime js, ElementReference element, Action callback)
public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback)
{
var interop = new SKHtmlCanvasInterop(js, element, callback);
await interop.ImportAsync();
return interop;
}
public SKHtmlCanvasInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback)
: base(js, JsFilename)
{
htmlCanvas = element;
htmlElementId = element.Id;
_module = module;
_htmlCanvas = element;
_htmlElementId = element.Id;
callbackHelper = new ActionHelper(renderFrameCallback);
_callbackHelper = new ActionHelper(renderFrameCallback);
}
protected override void OnDisposingModule() =>
Deinit();
public void Dispose() => Deinit();
public GLInfo InitGL()
{
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return Invoke<GLInfo>(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference);
return _module.Invoke<GLInfo>(InitGLSymbol, _htmlCanvas, _htmlElementId, callbackReference);
}
public bool InitRaster()
@ -54,9 +47,9 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference != null)
throw new InvalidOperationException("Unable to initialize the same canvas more than once.");
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
return Invoke<bool>(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference);
return _module.Invoke<bool>(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference);
}
public void Deinit()
@ -64,19 +57,19 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference == null)
return;
Invoke(DeinitSymbol, htmlElementId);
_module.Invoke(DeinitSymbol, _htmlElementId);
callbackReference?.Dispose();
}
public void RequestAnimationFrame(bool enableRenderLoop) =>
Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop);
_module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop);
public void SetCanvasSize(int rawWidth, int rawHeight) =>
Invoke(SetCanvasSizeSymbol, htmlCanvas, rawWidth, rawHeight);
_module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight);
public void PutImageData(IntPtr intPtr, SKSizeI rawSize) =>
Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
_module.Invoke(PutImageDataSymbol, _htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height);
public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth);
}

41
src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs

@ -1,50 +1,39 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using SkiaSharp;
namespace Avalonia.Web.Blazor.Interop
{
internal class SizeWatcherInterop : JSModuleInterop
internal class SizeWatcherInterop : IDisposable
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/SizeWatcher.js";
private const string ObserveSymbol = "SizeWatcher.observe";
private const string UnobserveSymbol = "SizeWatcher.unobserve";
private readonly ElementReference htmlElement;
private readonly string htmlElementId;
private readonly FloatFloatActionHelper callbackHelper;
private readonly AvaloniaModule _module;
private readonly ElementReference _htmlElement;
private readonly string _htmlElementId;
private readonly FloatFloatActionHelper _callbackHelper;
private DotNetObjectReference<FloatFloatActionHelper>? callbackReference;
public static async Task<SizeWatcherInterop> ImportAsync(IJSRuntime js, ElementReference element, Action<SKSize> callback)
public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action<SKSize> callback)
{
var interop = new SizeWatcherInterop(js, element, callback);
await interop.ImportAsync();
interop.Start();
return interop;
_module = module;
_htmlElement = element;
_htmlElementId = element.Id;
_callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
}
public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action<SKSize> callback)
: base(js, JsFilename)
{
htmlElement = element;
htmlElementId = element.Id;
callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y)));
}
protected override void OnDisposingModule() =>
Stop();
public void Dispose() => Stop();
public void Start()
{
if (callbackReference != null)
return;
callbackReference = DotNetObjectReference.Create(callbackHelper);
callbackReference = DotNetObjectReference.Create(_callbackHelper);
Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference);
_module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference);
}
public void Stop()
@ -52,7 +41,7 @@ namespace Avalonia.Web.Blazor.Interop
if (callbackReference == null)
return;
Invoke(UnobserveSymbol, htmlElementId);
_module.Invoke(UnobserveSymbol, _htmlElementId);
callbackReference?.Dispose();
callbackReference = null;

2
src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs

@ -12,7 +12,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage
internal class StorageProviderInterop : JSModuleInterop, IStorageProvider
{
private const string JsFilename = "./_content/Avalonia.Web.Blazor/StorageProvider.js";
private const string JsFilename = "./_content/Avalonia.Web.Blazor/avaloniaStorage.js";
private const string PickerCancelMessage = "The user aborted a request";
public static async Task<StorageProviderInterop> ImportAsync(IJSRuntime js)

41
src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts

@ -1,41 +0,0 @@

export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback: DotNet.DotNetObjectReference;
public static getDpi() {
return window.devicePixelRatio;
}
public static start(callback: DotNet.DotNetObjectReference): number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop() {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId);
DpiWatcher.callback = undefined;
}
static update() {
if (!DpiWatcher.callback)
return;
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
}
}
}

23
src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts

@ -1,23 +0,0 @@

export class InputHelper {
public static clear (inputElement: HTMLInputElement){
inputElement.value = "";
}
public static focus (inputElement: HTMLInputElement){
inputElement.focus();
inputElement.setSelectionRange(0, 0);
}
public static setCursor (inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static hide (inputElement: HTMLInputElement){
inputElement.style.display = 'none';
}
public static show (inputElement: HTMLInputElement){
inputElement.style.display = 'block';
}
}

261
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts

@ -1,261 +0,0 @@
// aliases for emscripten
declare let GL: any;
declare let GLctx: WebGLRenderingContext;
declare let Module: EmscriptenModule;
// container for gl info
type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
// alias for a potential skia html canvas
type SKHtmlCanvasElement = {
SKHtmlCanvas: SKHtmlCanvas
} & HTMLCanvasElement
export class SKHtmlCanvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObjectReference;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth: number;
newHeight: number;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo {
var view = SKHtmlCanvas.init(true, element, elementId, callback);
if (!view || !view.glInfo)
return null;
return view.glInfo;
}
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): boolean {
var view = SKHtmlCanvas.init(false, element, elementId, callback);
if (!view)
return false;
return true;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKHtmlCanvas {
var htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}
if (!SKHtmlCanvas.elements)
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
SKHtmlCanvas.elements[elementId] = element;
const view = new SKHtmlCanvas(useGL, element, callback);
htmlCanvas.SKHtmlCanvas = view;
return view;
}
public static deinit(elementId: string) {
if (!elementId)
return;
const element = SKHtmlCanvas.elements[elementId];
SKHtmlCanvas.elements.delete(elementId);
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.deinit();
htmlCanvas.SKHtmlCanvas = undefined;
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number)
{
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
}
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
}
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
}
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObjectReference) {
this.htmlCanvas = element;
this.renderFrameCallback = callback;
if (useGL) {
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return null;
}
// make current
GL.makeContextCurrent(ctx);
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.glInfo = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};
}
}
public deinit() {
this.setEnableRenderLoop(false);
}
public setCanvasSize(width: number, height: number)
{
this.newWidth = width;
this.newHeight = height;
if(this.htmlCanvas.width != this.newWidth)
{
this.htmlCanvas.width = this.newWidth;
}
if(this.htmlCanvas.height != this.newHeight)
{
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public requestAnimationFrame(renderLoop?: boolean) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if(this.htmlCanvas.width != this.newWidth)
{
this.htmlCanvas.width = this.newWidth;
}
if(this.htmlCanvas.height != this.newHeight)
{
this.htmlCanvas.height = this.newHeight;
}
this.renderFrameCallback.invokeMethod('Invoke');
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
}
public setEnableRenderLoop(enable: boolean) {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}
public putImageData(pData: number, width: number, height: number): boolean {
if (this.glInfo || !pData || width <= 0 || width <= 0)
return false;
var ctx = this.htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}
// make sure the canvas is scaled correctly for the drawing
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);
return true;
}
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 0,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1,
};
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}
return ctx;
}
}

68
src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts

@ -1,68 +0,0 @@

type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
type SizeWatcherInstance = {
callback: DotNet.DotNetObjectReference;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) {
if (!element || !callback)
return;
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback: callback
};
SizeWatcher.elements[elementId] = element;
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
return;
//console.info('Removing size watcher observation...');
const element = SizeWatcher.elements[elementId];
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
static init() {
if (SizeWatcher.observer)
return;
//console.info('Starting size watcher...');
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element) {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
return;
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
}
}

7
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts

@ -1,7 +0,0 @@
declare namespace DotNet {
interface DotNetObjectReference extends DotNet.DotNetObject {
_id: number;
dispose();
}
}

326
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts

@ -1,326 +0,0 @@
// Type definitions for Emscripten 1.39.16
// Project: https://emscripten.org
// Definitions by: Kensuke Matsuzaki <https://github.com/zakki>
// Periklis Tsirakidis <https://github.com/periklis>
// Bumsik Kim <https://github.com/kbumsik>
// Louis DeScioli <https://github.com/lourd>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.2
/** Other WebAssembly declarations, for compatibility with older versions of Typescript */
declare namespace WebAssembly {
interface Module {}
}
declare namespace Emscripten {
interface FileSystemType {}
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
type JSType = 'number' | 'string' | 'array' | 'boolean';
type TypeCompatibleWithC = number | string | any[] | boolean;
type CIntType = 'i8' | 'i16' | 'i32' | 'i64';
type CFloatType = 'float' | 'double';
type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*';
type CType = CIntType | CFloatType | CPointerType;
type WebAssemblyImports = Array<{
name: string;
kind: string;
}>;
type WebAssemblyExports = Array<{
module: string;
name: string;
kind: string;
}>;
interface CCallOpts {
async?: boolean | undefined;
}
}
interface EmscriptenModule {
print(str: string): void;
printErr(str: string): void;
arguments: string[];
environment: Emscripten.EnvironmentType;
preInit: Array<{ (): void }>;
preRun: Array<{ (): void }>;
postRun: Array<{ (): void }>;
onAbort: { (what: any): void };
onRuntimeInitialized: { (): void };
preinitializedWebGLContext: WebGLRenderingContext;
noInitialRun: boolean;
noExitRuntime: boolean;
logReadFiles: boolean;
filePackagePrefixURL: string;
wasmBinary: ArrayBuffer;
destroy(object: object): void;
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
instantiateWasm(
imports: Emscripten.WebAssemblyImports,
successCallback: (module: WebAssembly.Module) => void,
): Emscripten.WebAssemblyExports;
locateFile(url: string, scriptDirectory: string): string;
onCustomMessage(event: MessageEvent): void;
// USE_TYPED_ARRAYS == 1
HEAP: Int32Array;
IHEAP: Int32Array;
FHEAP: Float64Array;
// USE_TYPED_ARRAYS == 2
HEAP8: Int8Array;
HEAP16: Int16Array;
HEAP32: Int32Array;
HEAPU8: Uint8Array;
HEAPU16: Uint16Array;
HEAPU32: Uint32Array;
HEAPF32: Float32Array;
HEAPF64: Float64Array;
TOTAL_STACK: number;
TOTAL_MEMORY: number;
FAST_MEMORY: number;
addOnPreRun(cb: () => any): void;
addOnInit(cb: () => any): void;
addOnPreMain(cb: () => any): void;
addOnExit(cb: () => any): void;
addOnPostRun(cb: () => any): void;
preloadedImages: any;
preloadedAudios: any;
_malloc(size: number): number;
_free(ptr: number): void;
}
/**
* A factory function is generated when setting the `MODULARIZE` build option
* to `1` in your Emscripten build. It return a Promise that resolves to an
* initialized, ready-to-call `EmscriptenModule` instance.
*
* By default, the factory function will be named `Module`. It's recommended to
* use the `EXPORT_ES6` option, in which the factory function will be the
* default export. If used without `EXPORT_ES6`, the factory function will be a
* global variable. You can rename the variable using the `EXPORT_NAME` build
* option. It's left to you to declare any global variables as needed in your
* application's types.
* @param moduleOverrides Default properties for the initialized module.
*/
type EmscriptenModuleFactory<T extends EmscriptenModule = EmscriptenModule> = (
moduleOverrides?: Partial<T>,
) => Promise<T>;
declare namespace FS {
interface Lookup {
path: string;
node: FSNode;
}
interface FSStream {}
interface FSNode {}
interface ErrnoError {}
let ignorePermissions: boolean;
let trackingDelegate: any;
let tracking: any;
let genericErrors: any;
//
// paths
//
function lookupPath(path: string, opts: any): Lookup;
function getPath(node: FSNode): string;
//
// nodes
//
function isFile(mode: number): boolean;
function isDir(mode: number): boolean;
function isLink(mode: number): boolean;
function isChrdev(mode: number): boolean;
function isBlkdev(mode: number): boolean;
function isFIFO(mode: number): boolean;
function isSocket(mode: number): boolean;
//
// devices
//
function major(dev: number): number;
function minor(dev: number): number;
function makedev(ma: number, mi: number): number;
function registerDevice(dev: number, ops: any): void;
//
// core
//
function syncfs(populate: boolean, callback: (e: any) => any): void;
function syncfs(callback: (e: any) => any, populate?: boolean): void;
function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any;
function unmount(mountpoint: string): void;
function mkdir(path: string, mode?: number): any;
function mkdev(path: string, mode?: number, dev?: number): any;
function symlink(oldpath: string, newpath: string): any;
function rename(old_path: string, new_path: string): void;
function rmdir(path: string): void;
function readdir(path: string): any;
function unlink(path: string): void;
function readlink(path: string): string;
function stat(path: string, dontFollow?: boolean): any;
function lstat(path: string): any;
function chmod(path: string, mode: number, dontFollow?: boolean): void;
function lchmod(path: string, mode: number): void;
function fchmod(fd: number, mode: number): void;
function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void;
function lchown(path: string, uid: number, gid: number): void;
function fchown(fd: number, uid: number, gid: number): void;
function truncate(path: string, len: number): void;
function ftruncate(fd: number, len: number): void;
function utime(path: string, atime: number, mtime: number): void;
function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream;
function close(stream: FSStream): void;
function llseek(stream: FSStream, offset: number, whence: number): any;
function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number;
function write(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position?: number,
canOwn?: boolean,
): number;
function allocate(stream: FSStream, offset: number, length: number): void;
function mmap(
stream: FSStream,
buffer: ArrayBufferView,
offset: number,
length: number,
position: number,
prot: number,
flags: number,
): any;
function ioctl(stream: FSStream, cmd: any, arg: any): any;
function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array;
function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string;
function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array;
function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void;
//
// module-level FS code
//
function cwd(): string;
function chdir(path: string): void;
function init(
input: null | (() => number | null),
output: null | ((c: number) => any),
error: null | ((c: number) => any),
): void;
function createLazyFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
): FSNode;
function createPreloadedFile(
parent: string | FSNode,
name: string,
url: string,
canRead: boolean,
canWrite: boolean,
onload?: () => void,
onerror?: () => void,
dontCreateFile?: boolean,
canOwn?: boolean,
): void;
function createDataFile(
parent: string | FSNode,
name: string,
data: ArrayBufferView,
canRead: boolean,
canWrite: boolean,
canOwn: boolean,
): FSNode;
}
declare var MEMFS: Emscripten.FileSystemType;
declare var NODEFS: Emscripten.FileSystemType;
declare var IDBFS: Emscripten.FileSystemType;
// Below runtime function/variable declarations are exportable by
// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge
// EmscriptenModule interface to add runtime functions.
//
// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']"
// You can access ccall() via Module["ccall"]. In this case, you should
// extend EmscriptenModule to pass the compiler check like the following:
//
// interface YourOwnEmscriptenModule extends EmscriptenModule {
// ccall: typeof ccall;
// }
//
// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function
declare function ccall(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
args: Emscripten.TypeCompatibleWithC[],
opts?: Emscripten.CCallOpts,
): any;
declare function cwrap(
ident: string,
returnType: Emscripten.JSType | null,
argTypes: Emscripten.JSType[],
opts?: Emscripten.CCallOpts,
): (...args: any[]) => any;
declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void;
declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number;
declare function allocate(
slab: number[] | ArrayBufferView | number,
types: Emscripten.CType | Emscripten.CType[],
allocator: number,
ptr?: number,
): number;
declare function stackAlloc(size: number): number;
declare function stackSave(): number;
declare function stackRestore(ptr: number): void;
declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string;
declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF8(str: string): number;
declare function allocateUTF8(str: string): number;
declare function allocateUTF8OnStack(str: string): number;
declare function UTF16ToString(ptr: number): string;
declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF16(str: string): number;
declare function UTF32ToString(ptr: number): string;
declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void;
declare function lengthBytesUTF32(str: string): number;
declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
declare function intArrayToString(array: number[]): string;
declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function writeArrayToMemory(array: number[], buffer: number): void;
declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
declare function addRunDependency(id: any): void;
declare function removeRunDependency(id: any): void;
declare function addFunction(func: (...args: any[]) => any, signature?: string): number;
declare function removeFunction(funcPtr: number): void;
declare var ALLOC_NORMAL: number;
declare var ALLOC_STACK: number;
declare var ALLOC_STATIC: number;
declare var ALLOC_DYNAMIC: number;
declare var ALLOC_NONE: number;

14
src/Web/Avalonia.Web.Blazor/tsconfig.json

@ -1,14 +0,0 @@
{
"compilerOptions": {
"noImplicitAny": false,
"noEmitOnError": true,
"removeComments": false,
"sourceMap": true,
"target": "ES2020",
"module": "ES2020",
"outDir": "wwwroot"
},
"exclude": [
"node_modules"
]
}

5
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts

@ -0,0 +1,5 @@
export { DpiWatcher } from "./DpiWatcher"
export { InputHelper } from "./InputHelper"
export { NativeControlHost } from "./NativeControlHost"
export { SizeWatcher } from "./SizeWatcher"
export { SKHtmlCanvas } from "./SKHtmlCanvas"

40
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts

@ -0,0 +1,40 @@
export class DpiWatcher {
static lastDpi: number;
static timerId: number;
static callback?: DotNet.DotNetObject;
public static getDpi() {
return window.devicePixelRatio;
}
public static start(callback: DotNet.DotNetObject): number {
//console.info(`Starting DPI watcher with callback ${callback._id}...`);
DpiWatcher.lastDpi = window.devicePixelRatio;
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000);
DpiWatcher.callback = callback;
return DpiWatcher.lastDpi;
}
public static stop() {
//console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`);
window.clearInterval(DpiWatcher.timerId);
DpiWatcher.callback = undefined;
}
static update() {
if (!DpiWatcher.callback)
return;
const currentDpi = window.devicePixelRatio;
const lastDpi = DpiWatcher.lastDpi;
DpiWatcher.lastDpi = currentDpi;
if (Math.abs(lastDpi - currentDpi) > 0.001) {
DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi);
}
}
}

22
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts

@ -0,0 +1,22 @@
export class InputHelper {
public static clear(inputElement: HTMLInputElement) {
inputElement.value = "";
}
public static focus(inputElement: HTMLInputElement) {
inputElement.focus();
inputElement.setSelectionRange(0, 0);
}
public static setCursor(inputElement: HTMLInputElement, kind: string) {
inputElement.style.cursor = kind;
}
public static hide(inputElement: HTMLInputElement) {
inputElement.style.display = 'none';
}
public static show(inputElement: HTMLInputElement) {
inputElement.style.display = 'block';
}
}

35
src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts → src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts

@ -14,10 +14,9 @@
}
}
class NativeControlHostTopLevelAttachment
{
_child: HTMLElement;
_host: HTMLElement;
class NativeControlHostTopLevelAttachment {
_child?: HTMLElement;
_host?: HTMLElement;
InitializeWithChildHandle(child: HTMLElement) {
this._child = child;
@ -25,32 +24,38 @@ class NativeControlHostTopLevelAttachment
}
AttachTo(host: HTMLElement): void {
if (this._host) {
if (this._host && this._child) {
this._host.removeChild(this._child);
}
this._host = host;
if (this._host) {
if (this._host && this._child) {
this._host.appendChild(this._child);
}
}
ShowInBounds(x: number, y: number, width: number, height: number): void {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
if (this._child) {
this._child.style.top = y + "px";
this._child.style.left = x + "px";
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "block";
}
}
HideWithSize(width: number, height: number): void {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
if (this._child) {
this._child.style.width = width + "px";
this._child.style.height = height + "px";
this._child.style.display = "none";
}
}
ReleaseChild(): void {
this._child = null;
if (this._child) {
this._child = undefined;
}
}
}

255
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts

@ -0,0 +1,255 @@
// aliases for emscripten
declare let GL: any;
declare let GLctx: WebGLRenderingContext;
declare let Module: EmscriptenModule;
// container for gl info
type SKGLViewInfo = {
context: WebGLRenderingContext | WebGL2RenderingContext | undefined;
fboId: number;
stencil: number;
sample: number;
depth: number;
}
// alias for a potential skia html canvas
type SKHtmlCanvasElement = {
SKHtmlCanvas: SKHtmlCanvas | undefined
} & HTMLCanvasElement
export class SKHtmlCanvas {
static elements: Map<string, HTMLCanvasElement>;
htmlCanvas: HTMLCanvasElement;
glInfo?: SKGLViewInfo;
renderFrameCallback: DotNet.DotNetObject;
renderLoopEnabled: boolean = false;
renderLoopRequest: number = 0;
newWidth?: number;
newHeight?: number;
public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null {
var view = SKHtmlCanvas.init(true, element, elementId, callback);
if (!view || !view.glInfo)
return null;
return view.glInfo;
}
public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean {
var view = SKHtmlCanvas.init(false, element, elementId, callback);
if (!view)
return false;
return true;
}
static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null {
var htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas) {
console.error(`No canvas element was provided.`);
return null;
}
if (!SKHtmlCanvas.elements)
SKHtmlCanvas.elements = new Map<string, HTMLCanvasElement>();
SKHtmlCanvas.elements.set(elementId, element);
const view = new SKHtmlCanvas(useGL, element, callback);
htmlCanvas.SKHtmlCanvas = view;
return view;
}
public static deinit(elementId: string) {
if (!elementId)
return;
const element = SKHtmlCanvas.elements.get(elementId);
SKHtmlCanvas.elements.delete(elementId);
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.deinit();
htmlCanvas.SKHtmlCanvas = undefined;
}
public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop);
}
public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height);
}
public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable);
}
public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) {
const htmlCanvas = element as SKHtmlCanvasElement;
if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas)
return;
htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height);
}
public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) {
this.htmlCanvas = element;
this.renderFrameCallback = callback;
if (useGL) {
const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas);
if (!ctx) {
console.error(`Failed to create WebGL context: err ${ctx}`);
return;
}
// make current
GL.makeContextCurrent(ctx);
// read values
const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING);
this.glInfo = {
context: ctx,
fboId: fbo ? fbo.id : 0,
stencil: GLctx.getParameter(GLctx.STENCIL_BITS),
sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES)
depth: GLctx.getParameter(GLctx.DEPTH_BITS),
};
}
}
public deinit() {
this.setEnableRenderLoop(false);
}
public setCanvasSize(width: number, height: number) {
this.newWidth = width;
this.newHeight = height;
if (this.htmlCanvas.width != this.newWidth) {
this.htmlCanvas.width = this.newWidth;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight;
}
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
}
public requestAnimationFrame(renderLoop?: boolean) {
// optionally update the render loop
if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop)
this.setEnableRenderLoop(renderLoop);
// skip because we have a render loop
if (this.renderLoopRequest !== 0)
return;
// add the draw to the next frame
this.renderLoopRequest = window.requestAnimationFrame(() => {
if (this.glInfo) {
// make current
GL.makeContextCurrent(this.glInfo.context);
}
if (this.htmlCanvas.width != this.newWidth) {
this.htmlCanvas.width = this.newWidth || 0;
}
if (this.htmlCanvas.height != this.newHeight) {
this.htmlCanvas.height = this.newHeight || 0;
}
this.renderFrameCallback.invokeMethod('Invoke');
this.renderLoopRequest = 0;
// we may want to draw the next frame
if (this.renderLoopEnabled)
this.requestAnimationFrame();
});
}
public setEnableRenderLoop(enable: boolean) {
this.renderLoopEnabled = enable;
// either start the new frame or cancel the existing one
if (enable) {
//console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`);
this.requestAnimationFrame();
} else if (this.renderLoopRequest !== 0) {
window.cancelAnimationFrame(this.renderLoopRequest);
this.renderLoopRequest = 0;
}
}
public putImageData(pData: number, width: number, height: number): boolean {
if (this.glInfo || !pData || width <= 0 || width <= 0)
return false;
var ctx = this.htmlCanvas.getContext('2d');
if (!ctx) {
console.error(`Failed to obtain 2D canvas context.`);
return false;
}
// make sure the canvas is scaled correctly for the drawing
this.htmlCanvas.width = width;
this.htmlCanvas.height = height;
// set the canvas to be the bytes
var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4);
var imageData = new ImageData(buffer, width, height);
ctx.putImageData(imageData, 0, 0);
return true;
}
static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext {
const contextAttributes = {
alpha: 1,
depth: 1,
stencil: 8,
antialias: 0,
premultipliedAlpha: 1,
preserveDrawingBuffer: 0,
preferLowPowerToHighPerformance: 0,
failIfMajorPerformanceCaveat: 0,
majorVersion: 2,
minorVersion: 0,
enableExtensionsByDefault: 1,
explicitSwapControl: 0,
renderViaOffscreenBackBuffer: 1,
};
let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes);
if (!ctx && contextAttributes.majorVersion > 1) {
console.warn('Falling back to WebGL 1.0');
contextAttributes.majorVersion = 1;
contextAttributes.minorVersion = 0;
ctx = GL.createContext(htmlCanvas, contextAttributes);
}
return ctx;
}
}

67
src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts

@ -0,0 +1,67 @@
type SizeWatcherElement = {
SizeWatcher: SizeWatcherInstance;
} & HTMLElement
type SizeWatcherInstance = {
callback: DotNet.DotNetObject;
}
export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) {
if (!element || !callback)
return;
//console.info(`Adding size watcher observation with callback ${callback._id}...`);
SizeWatcher.init();
const watcherElement = element as SizeWatcherElement;
watcherElement.SizeWatcher = {
callback: callback
};
SizeWatcher.elements.set(elementId, element);
SizeWatcher.observer.observe(element);
SizeWatcher.invoke(element);
}
public static unobserve(elementId: string) {
if (!elementId || !SizeWatcher.observer)
return;
//console.info('Removing size watcher observation...');
const element = SizeWatcher.elements.get(elementId)!;
SizeWatcher.elements.delete(elementId);
SizeWatcher.observer.unobserve(element);
}
static init() {
if (SizeWatcher.observer)
return;
//console.info('Starting size watcher...');
SizeWatcher.elements = new Map<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (let entry of entries) {
SizeWatcher.invoke(entry.target);
}
});
}
static invoke(element: Element) {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback)
return;
return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight);
}
}

79
src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts

@ -0,0 +1,79 @@
class InnerDbConnection {
constructor(private database: IDBDatabase) { }
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
const tx = this.database.transaction(store, mode);
return tx.objectStore(store);
}
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.put(obj, key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public get(store: string, key: IDBValidKey): any {
const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => {
const response = os.get(key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public delete(store: string, key: IDBValidKey): Promise<void> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.delete(key);
response.onsuccess = () => {
resolve();
};
response.onerror = () => {
reject(response.error);
};
});
}
public close() {
this.database.close();
}
}
export class IndexedDbWrapper {
constructor(private databaseName: string, private objectStores: [string]) {
}
public connect(): Promise<InnerDbConnection> {
const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => {
const db = (<IDBRequest<IDBDatabase>>event.target).result;
this.objectStores.forEach(store => {
db.createObjectStore(store);
});
};
return new Promise((resolve, reject) => {
conn.onsuccess = event => {
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
};
conn.onerror = event => {
reject((<IDBRequest<IDBDatabase>>event.target).error);
};
});
}
}

172
src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts → src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts

@ -1,141 +1,23 @@
// As we don't have proper package managing for Avalonia.Web project, declare types manually
declare global {
interface FileSystemWritableFileStream {
write(position: number, data: BufferSource | Blob | string): Promise<void>;
truncate(size: number): Promise<void>;
close(): Promise<void>;
}
type PermissionsMode = "read" | "readwrite";
interface FileSystemFileHandle {
name: string,
getFile(): Promise<File>;
createWritable(options?: { keepExistingData?: boolean }): Promise<FileSystemWritableFileStream>;
queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">;
import { IndexedDbWrapper } from "./IndexedDbWrapper";
entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>;
}
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemFileHandle;
interface FilePickerAcceptType {
description: string,
// mime -> ext[] array
accept: { [mime: string]: string | string[] }
}
interface FilePickerOptions {
types?: FilePickerAcceptType[],
excludeAcceptAllOption: boolean,
id?: string,
declare global {
type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos";
type StartInDirectory = WellKnownDirectory | FileSystemHandle;
interface OpenFilePickerOptions {
startIn?: StartInDirectory
}
interface OpenFilePickerOptions extends FilePickerOptions {
multiple: boolean
}
interface SaveFilePickerOptions extends FilePickerOptions {
suggestedName?: string
}
interface DirectoryPickerOptions {
id?: string,
interface SaveFilePickerOptions {
startIn?: StartInDirectory
}
interface Window {
showOpenFilePicker: (options: OpenFilePickerOptions) => Promise<FileSystemFileHandle[]>;
showSaveFilePicker: (options: SaveFilePickerOptions) => Promise<FileSystemFileHandle>;
showDirectoryPicker: (options: DirectoryPickerOptions) => Promise<FileSystemFileHandle>;
}
}
// TODO move to another file and use import
class IndexedDbWrapper {
constructor(private databaseName: string, private objectStores: [ string ]) {
}
public connect(): Promise<InnerDbConnection> {
const conn = window.indexedDB.open(this.databaseName, 1);
conn.onupgradeneeded = event => {
const db = (<IDBRequest<IDBDatabase>>event.target).result;
this.objectStores.forEach(store => {
db.createObjectStore(store);
});
}
return new Promise((resolve, reject) => {
conn.onsuccess = event => {
resolve(new InnerDbConnection((<IDBRequest<IDBDatabase>>event.target).result));
}
conn.onerror = event => {
reject((<IDBRequest<IDBDatabase>>event.target).error);
}
});
}
}
class InnerDbConnection {
constructor(private database: IDBDatabase) { }
private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore {
const tx = this.database.transaction(store, mode);
return tx.objectStore(store);
}
public put(store: string, obj: any, key?: IDBValidKey): Promise<IDBValidKey> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.put(obj, key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public get(store: string, key: IDBValidKey): any {
const os = this.openStore(store, "readonly");
return new Promise((resolve, reject) => {
const response = os.get(key);
response.onsuccess = () => {
resolve(response.result);
};
response.onerror = () => {
reject(response.error);
};
});
}
public delete(store: string, key: IDBValidKey): Promise<void> {
const os = this.openStore(store, "readwrite");
return new Promise((resolve, reject) => {
const response = os.delete(key);
response.onsuccess = () => {
resolve();
};
response.onerror = () => {
reject(response.error);
};
});
}
public close() {
this.database.close();
}
}
const fileBookmarksStore: string = "fileBookmarks";
const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [
fileBookmarksStore
])
]);
class StorageItem {
constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { }
constructor(public handle: FileSystemHandle, private bookmarkId?: string) { }
public getName(): string {
return this.handle.name
@ -146,21 +28,35 @@ class StorageItem {
}
public async openRead(): Promise<Blob> {
if (!(this.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await this.verityPermissions('read');
return await this.handle.getFile();
const file = await this.handle.getFile();
return file;
}
public async openWrite(): Promise<FileSystemWritableFileStream> {
if (!(this.handle instanceof FileSystemFileHandle)) {
throw new Error("StorageItem is not a file");
}
await this.verityPermissions('readwrite');
return await this.handle.createWritable({ keepExistingData: true });
}
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string }> {
const file = this.handle.getFile && await this.handle.getFile();
return file && {
public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> {
const file = this.handle instanceof FileSystemFileHandle
&& await this.handle.getFile();
if (!file) {
return null;
}
return {
Size: file.size,
LastModified: file.lastModified,
Type: file.type
@ -168,14 +64,18 @@ class StorageItem {
}
public async getItems(): Promise<StorageItems> {
if (this.handle.kind !== "directory"){
return new StorageItems([]);
}
const items: StorageItem[] = [];
for await (const [key, value] of this.handle.entries()) {
for await (const [key, value] of (this.handle as any).entries()) {
items.push(new StorageItem(value));
}
return new StorageItems(items);
}
private async verityPermissions(mode: PermissionsMode): Promise<void | never> {
private async verityPermissions(mode: FileSystemPermissionMode): Promise<void | never> {
if (await this.handle.queryPermission({ mode }) === 'granted') {
return;
}
@ -190,7 +90,7 @@ class StorageItem {
if (this.bookmarkId) {
return this.bookmarkId;
}
const connection = await avaloniaDb.connect();
try {
const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId());
@ -200,7 +100,7 @@ class StorageItem {
connection.close();
}
}
public async deleteBookmark(): Promise<void> {
if (!this.bookmarkId) {
return;
@ -272,7 +172,7 @@ export class StorageProvider {
};
const handles = await window.showOpenFilePicker(options);
return new StorageItems(handles.map(handle => new StorageItem(handle)));
return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle)));
}
public static async saveFileDialog(

16
src/Web/Avalonia.Web.Blazor/webapp/package.json

@ -0,0 +1,16 @@
{
"name": "avalonia.web",
"scripts": {
"dist": "cross-env NODE_ENV=production webpack",
"build": "cross-env NODE_ENV=development webpack"
},
"devDependencies": {
"@types/emscripten": "^1.39.6",
"@types/wicg-file-system-access": "^2020.9.5",
"cross-env": "^7.0.3",
"ts-loader": "^9.3.1",
"typescript": "^4.7.4",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0"
}
}

18
src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES6",
"strict": true,
"sourceMap": true,
"outDir": "../wwwroot",
"removeComments": true,
"noEmitOnError": true,
"lib": [
"dom",
"ES6",
"esnext.asynciterable"
]
},
"exclude": [
"node_modules"
]
}

0
src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts → src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts

40
src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js

@ -0,0 +1,40 @@
const path = require('path');
const prod = process.env.NODE_ENV == 'production';
module.exports = {
mode: prod ? "production" : "development",
devtool: 'source-map',
target: ["web", "es2020"],
entry: {
avalonia: './modules/Avalonia/Avalonia.ts',
avaloniaStorage: {
import: './modules/Storage/StorageProvider.ts',
dependOn: 'avalonia',
}
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../wwwroot'),
library: {
type: 'module',
},
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
optimization: {
minimize: false
},
experiments: {
outputModule: true,
}
};
Loading…
Cancel
Save