{"id":13629184,"url":"https://github.com/IEvangelist/blazorators","last_synced_at":"2025-04-17T04:33:09.953Z","repository":{"id":37764489,"uuid":"416383251","full_name":"IEvangelist/blazorators","owner":"IEvangelist","description":"This project converts TypeScript type declarations into C# representations, and use C# source generators to expose automatic JavaScript interop functionality.","archived":false,"fork":false,"pushed_at":"2024-12-01T02:39:23.000Z","size":97487,"stargazers_count":366,"open_issues_count":1,"forks_count":31,"subscribers_count":15,"default_branch":"main","last_synced_at":"2025-04-05T21:16:16.980Z","etag":null,"topics":["blazor","blazor-webassembly","hacktoberfest","javascript","javascript-interop","source-generators","typescript"],"latest_commit_sha":null,"homepage":"https://ievangelist.github.io/blazorators","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/IEvangelist.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"IEvangelist","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2021-10-12T15:02:44.000Z","updated_at":"2025-04-04T13:27:28.000Z","dependencies_parsed_at":"2024-07-17T04:27:48.350Z","dependency_job_id":"0a7cd1ab-1ea8-4584-b724-a1a653fe6fa2","html_url":"https://github.com/IEvangelist/blazorators","commit_stats":{"total_commits":195,"total_committers":10,"mean_commits":19.5,"dds":"0.18461538461538463","last_synced_commit":"dfa8d01d4795afbbbb9048800f4d98a50327388a"},"previous_names":[],"tags_count":40,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IEvangelist%2Fblazorators","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IEvangelist%2Fblazorators/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IEvangelist%2Fblazorators/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IEvangelist%2Fblazorators/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/IEvangelist","download_url":"https://codeload.github.com/IEvangelist/blazorators/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248223823,"owners_count":21068069,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blazor","blazor-webassembly","hacktoberfest","javascript","javascript-interop","source-generators","typescript"],"created_at":"2024-08-01T22:01:04.033Z","updated_at":"2025-04-17T04:33:09.901Z","avatar_url":"https://github.com/IEvangelist.png","language":"C#","readme":"\u003cimg src=\"https://raw.githubusercontent.com/IEvangelist/blazorators/main/logo.png\" align=\"right\"\u003e\u003c/img\u003e\r\n# Blazorators: Blazor C# Source Generators\r\n\r\n\u003e Thank you for perusing my Blazor C# Source Generator repository. I'd really appreciate a ⭐ if you find this interesting.\r\n\r\n[![build](https://github.com/IEvangelist/blazorators/actions/workflows/build-validation.yml/badge.svg)](https://github.com/IEvangelist/blazorators/actions/workflows/build-validation.yml)\r\n\u003c!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --\u003e\n[![All Contributors](https://img.shields.io/badge/all_contributors-11-orange.svg?style=flat-square)](#contributors-)\n\u003c!-- ALL-CONTRIBUTORS-BADGE:END --\u003e\r\n\r\n\u003c!--\r\n## Stats\r\n\r\n![Alt](https://repobeats.axiom.co/api/embed/6d5b98efd6598a4482f1fa6391fbe651a06f77d9.svg \"Repobeats analytics image\")\r\n--\u003e\r\n\r\nA C# source generator that creates fully functioning Blazor JavaScript interop code, targeting either the `IJSInProcessRuntime` or `IJSRuntime` types. This library provides several NuGet packages:\r\n\r\n**Core libraries**\r\n\r\n| NuGet package | NuGet version | Description |\r\n|--|--|--|\r\n| [`Blazor.SourceGenerators`](https://www.nuget.org/packages/Blazor.SourceGenerators) | [![NuGet](https://img.shields.io/nuget/v/Blazor.SourceGenerators.svg?style=flat)](https://www.nuget.org/packages/Blazor.SourceGenerators) | Core source generator library. |\r\n| [`Blazor.Serialization`](https://www.nuget.org/packages/Blazor.Serialization) | [![NuGet](https://img.shields.io/nuget/v/Blazor.Serialization.svg?style=flat)](https://www.nuget.org/packages/Blazor.Serialization) | Common serialization library, required in some scenarios when using generics. |\r\n\r\n**WebAssembly libraries**\r\n\r\n| NuGet package | NuGet version | Description |\r\n|--|--|--|\r\n| [`Blazor.LocalStorage.WebAssembly`](https://www.nuget.org/packages/Blazor.LocalStorage.WebAssembly) | [![NuGet](https://img.shields.io/nuget/v/Blazor.LocalStorage.WebAssembly.svg?style=flat)](https://www.nuget.org/packages/Blazor.LocalStorage.WebAssembly) | Blazor WebAssembly class library exposing DI-ready `IStorageService` type for the `localStorage` implementation (relies on `IJSInProcessRuntime`). |\r\n| [`Blazor.SessionStorage.WebAssembly`](https://www.nuget.org/packages/Blazor.SessionStorage.WebAssembly) | [![NuGet](https://img.shields.io/nuget/v/Blazor.SessionStorage.WebAssembly.svg?style=flat)](https://www.nuget.org/packages/Blazor.SessionStorage.WebAssembly) | Blazor WebAssembly class library exposing DI-ready `IStorageService` type for the `sessionStorage` implementation (relies on `IJSInProcessRuntime`). |\r\n| [`Blazor.Geolocation.WebAssembly`](https://www.nuget.org/packages/Blazor.Geolocation.WebAssembly) | [![NuGet](https://img.shields.io/nuget/v/Blazor.Geolocation.WebAssembly.svg?style=flat)](https://www.nuget.org/packages/Blazor.Geolocation.WebAssembly) | Razor class library exposing DI-ready `IGeolocationService` type (and dependent callback types) for the `geolocation` implementation (relies on `IJSInProcessRuntime`). |\r\n| [`Blazor.SpeechSynthesis.WebAssembly`](https://www.nuget.org/packages/Blazor.SpeechSynthesis.WebAssembly) | [![NuGet](https://img.shields.io/nuget/v/Blazor.SpeechSynthesis.WebAssembly.svg?style=flat)](https://www.nuget.org/packages/Blazor.SpeechSynthesis.WebAssembly) | Razor class library exposing DI-ready `ISpeechSynthesisService` type for the `speechSynthesis` implementation (relies on `IJSInProcessRuntime`). |\r\n\r\n\u003e Targets the `IJSInProcessRuntime` type.\r\n\r\n**Server libraries**\r\n\r\n| NuGet package | NuGet version | Description |\r\n|--|--|--|\r\n| [`Blazor.LocalStorage`](https://www.nuget.org/packages/Blazor.LocalStorage) | [![NuGet](https://img.shields.io/nuget/v/Blazor.LocalStorage.svg?style=flat)](https://www.nuget.org/packages/Blazor.LocalStorage) | Blazor Server class library exposing DI-ready `IStorageService` type for the `localStorage` implementation (relies on `IJSRuntime`) |\r\n| [`Blazor.SessionStorage`](https://www.nuget.org/packages/Blazor.SessionStorage) | [![NuGet](https://img.shields.io/nuget/v/Blazor.SessionStorage.svg?style=flat)](https://www.nuget.org/packages/Blazor.SessionStorage) | Blazor Server class library exposing DI-ready `IStorageService` type for the `sessionStorage` implementation (relies on `IJSRuntime`) |\r\n| [`Blazor.Geolocation`](https://www.nuget.org/packages/Blazor.Geolocation) | [![NuGet](https://img.shields.io/nuget/v/Blazor.Geolocation.svg?style=flat)](https://www.nuget.org/packages/Blazor.Geolocation) | Razor class library exposing DI-ready `IGeolocationService` type (and dependent callback types) for the `geolocation` implementation (relies on `IJSRuntime`). |\r\n| [`Blazor.SpeechSynthesis`](https://www.nuget.org/packages/Blazor.SpeechSynthesis) | [![NuGet](https://img.shields.io/nuget/v/Blazor.SpeechSynthesis.svg?style=flat)](https://www.nuget.org/packages/Blazor.SpeechSynthesis) | Razor class library exposing DI-ready `ISpeechSynthesisService` type for the `speechSynthesis` implementation (relies on `IJSRuntime`). |\r\n\r\n\u003e Targets the `IJSRuntime` type.\r\n\r\n\u003e **Note**\u003cbr\u003e\n\u003e The reason that I generate two separate packages, one with an async API and another with the synchronous version is due to the explicit usage of `IJSInProcessRuntime` when using Blazor WebAssembly. This decision allows the APIs to be separate, and easily consumable from their repsective consuming Blazor apps, either Blazor server or Blazor WebAssembly. I might change it later to make this a consumer configuration, in that each consuming library will have to explicitly define a preprocessor directive to specify `IS_WEB_ASSEMBLY` defined.\r\n\r\n## Using the `Blazor.SourceGenerators` package 📦\r\n\r\nAs an example, the official [`Blazor.LocalStorage.WebAssembly`](https://www.nuget.org/packages/Blazor.LocalStorage.WebAssembly) package consumes the [`Blazor.SourceGenerators`](https://www.nuget.org/packages/Blazor.SourceGenerators) package. It exposes extension methods specific to Blazor WebAssembly and the [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage) Web API.\r\n\r\nConsider the _IStorageService.cs_ C# file:\r\n\r\n```csharp\r\n// Copyright (c) David Pine. All rights reserved.\r\n// Licensed under the MIT License.\r\n\r\nnamespace Microsoft.JSInterop;\r\n\r\n[JSAutoGenericInterop(\r\n    TypeName = \"Storage\",\r\n    Implementation = \"window.localStorage\",\r\n    Url = \"https://developer.mozilla.org/docs/Web/API/Window/localStorage\",\r\n    GenericMethodDescriptors = new[]\r\n    {\r\n        \"getItem\",\r\n        \"setItem:value\"\r\n    })]\r\npublic partial interface IStorageService\r\n{\r\n}\r\n```\r\n\r\nThis code designates itself into the `Microsoft.JSInterop` namespace, making the source generated implementation available to anyone consumer who uses types from this namespace. It uses the `JSAutoGenericInterop` to specify:\r\n\r\n- `TypeName = \"Storage\"`: sets the type to [`Storage`](https://developer.mozilla.org/docs/Web/API/Storage).\r\n- `Implementation = \"window.localStorage\"`: expresses how to locate the implementation of the specified type from the globally scoped `window` object, this is the [`localStorage`](https://developer.mozilla.org/docs/Web/API/Window/localStorage) implementation.\r\n- `Url`: sets the URL for the implementation.\r\n- `GenericMethodDescriptors`: Defines the methods that should support generics as part of their source-generation. The `localStorage.getItem` is specified to return a generic `TResult` type, and the `localStorage.setItem` has its parameter with a name of `value` specified as a generic `TArg` type.\r\n\r\n\u003e The generic method descriptors syntax is:\r\n\u003e `\"methodName\"` for generic return type and `\"methodName:parameterName\"` for generic parameter type.\r\n\r\nThe file needs to define an interface and it needs to be `partial`, for example; `public partial interface`. Decorating the class with the `JSAutoInterop` (or `JSAutoGenericInterop) attribute will source generate the following C# code, as shown in the source generated _IStorageServiceService.g.cs_:\r\n\r\n```csharp\r\nusing Blazor.Serialization.Extensions;\r\nusing System.Text.Json;\r\n\r\n#nullable enable\r\nnamespace Microsoft.JSInterop;\r\n\r\n/// \u003csummary\u003e\r\n/// Source generated interface definition of the \u003cc\u003eStorage\u003c/c\u003e type.\r\n/// \u003c/summary\u003e\r\npublic partial interface IStorageServiceService\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.length\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/length\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    double Length\r\n    {\r\n        get;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.clear\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/clear\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    void Clear();\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.getItem\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/getItem\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    TValue? GetItem\u003cTValue\u003e(string key, JsonSerializerOptions? options = null);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.key\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/key\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    string? Key(double index);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.removeItem\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/removeItem\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    void RemoveItem(string key);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.setItem\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/setItem\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    void SetItem\u003cTValue\u003e(string key, TValue value, JsonSerializerOptions? options = null);\r\n}\r\n```\r\n\r\nThese internal extension methods rely on the `IJSInProcessRuntime` to perform JavaScript interop. From the given `TypeName` and corresponding `Implementation`, the following code is also generated:\r\n\r\n- `IStorageService.g.cs`: The interface for the corresponding `Storage` Web API surface area.\r\n- `LocalStorgeService.g.cs`: The `internal` implementation of the `IStorageService` interface.\r\n- `LocalStorageServiceCollectionExtensions.g.cs`: Extension methods to add the `IStorageService` service to the dependency injection `IServiceCollection`.\r\n\r\nHere is the source generated `LocalStorageService` implementation:\r\n\r\n```csharp\r\n// Copyright (c) David Pine. All rights reserved.\r\n// Licensed under the MIT License:\r\n// https://github.com/IEvangelist/blazorators/blob/main/LICENSE\r\n// Auto-generated by blazorators.\r\n\r\n#nullable enable\r\n\r\nusing Blazor.Serialization.Extensions;\r\nusing Microsoft.JSInterop;\r\nusing System.Text.Json;\r\n\r\nnamespace Microsoft.JSInterop;\r\n\r\n/// \u003cinheritdoc /\u003e\r\ninternal sealed class LocalStorageService : IStorageService\r\n{\r\n    private readonly IJSInProcessRuntime _javaScript = null;\r\n\r\n    /// \u003cinheritdoc cref=\"P:Microsoft.JSInterop.IStorageService.Length\" /\u003e\r\n    double IStorageService.Length =\u003e _javaScript.Invoke\u003cdouble\u003e(\"eval\", new object[1]\r\n    {\r\n        \"window.localStorage.length\"\r\n    });\r\n\r\n    public LocalStorageService(IJSInProcessRuntime javaScript)\r\n    {\r\n        _javaScript = javaScript;\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IStorageService.Clear\" /\u003e\r\n    void IStorageService.Clear()\r\n    {\r\n        _javaScript.InvokeVoid(\"window.localStorage.clear\");\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IStorageService.GetItem``1(System.String,System.Text.Json.JsonSerializerOptions)\" /\u003e\r\n    TValue? IStorageService.GetItem\u003cTValue\u003e(string key, JsonSerializerOptions? options)\r\n    {\r\n        return _javaScript.Invoke\u003cstring\u003e(\"window.localStorage.getItem\", new object[1]\r\n        {\r\n            key\r\n        }).FromJson\u003cTValue\u003e(options);\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IStorageService.Key(System.Double)\" /\u003e\r\n    string? IStorageService.Key(double index)\r\n    {\r\n        return _javaScript.Invoke\u003cstring\u003e(\"window.localStorage.key\", new object[1]\r\n        {\r\n            index\r\n        });\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IStorageService.RemoveItem(System.String)\" /\u003e\r\n    void IStorageService.RemoveItem(string key)\r\n    {\r\n        _javaScript.InvokeVoid(\"window.localStorage.removeItem\", key);\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IStorageService.SetItem``1(System.String,``0,System.Text.Json.JsonSerializerOptions)\" /\u003e\r\n    void IStorageService.SetItem\u003cTValue\u003e(string key, TValue value, JsonSerializerOptions? options)\r\n    {\r\n        _javaScript.InvokeVoid(\"window.localStorage.setItem\", key, value.ToJson\u003cTValue\u003e(options));\r\n    }\r\n}\r\n```\r\n\r\nFinally, here is the source generated service collection extension methods:\r\n\r\n```csharp\r\nusing Microsoft.JSInterop;\r\n\r\nnamespace Microsoft.Extensions.DependencyInjection;\r\n\r\n/// \u003csummary\u003e\u003c/summary\u003e\r\npublic static class LocalStorageServiceCollectionExtensions\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Adds the \u003csee cref=\"IStorageService\" /\u003e service to the service collection.\r\n    /// \u003c/summary\u003e\r\n    public static IServiceCollection AddLocalStorageServices(\r\n        this IServiceCollection services) =\u003e\r\n        services.AddSingleton\u003cIJSInProcessRuntime\u003e(serviceProvider =\u003e\r\n            (IJSInProcessRuntime)serviceProvider.GetRequiredService\u003cIJSRuntime\u003e())\r\n            .AddSingleton\u003cIStorageService, LocalStorageService\u003e();\r\n}\r\n```\r\n\r\nPutting this all together, the `Blazor.LocalStorage.WebAssembly` NuGet package is actually less than 15 lines of code, and it generates full DI-ready services with JavaScript interop.\r\n\r\nThe `Blazor.LocalStorage` package, generates extensions on the `IJSRuntime` type.\r\n\r\n```csharp\r\n// Copyright (c) David Pine. All rights reserved.\r\n// Licensed under the MIT License.\r\n\r\nnamespace Microsoft.JSInterop;\r\n\r\n[JSAutoInterop(\r\n    TypeName = \"Storage\",\r\n    Implementation = \"window.localStorage\",\r\n    HostingModel = BlazorHostingModel.Server,\r\n    OnlyGeneratePureJS = true,\r\n    Url = \"https://developer.mozilla.org/docs/Web/API/Window/localStorage\")]\r\npublic partial interface IStorageServiceService\r\n{\r\n}\r\n```\r\n\r\nGenerates the following:\r\n\r\n```csharp\r\n// Copyright (c) David Pine. All rights reserved.\r\n// Licensed under the MIT License:\r\n// https://github.com/IEvangelist/blazorators/blob/main/LICENSE\r\n// Auto-generated by blazorators.\r\n\r\nusing System.Threading.Tasks;\r\n\r\n#nullable enable\r\nnamespace Microsoft.JSInterop;\r\n\r\npublic partial interface IStorageServiceService\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.length\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/length\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    ValueTask\u003cdouble\u003e Length\r\n    {\r\n        get;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.clear\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/clear\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    ValueTask ClearAsync();\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.getItem\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/getItem\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    ValueTask\u003cstring?\u003e GetItemAsync(string key);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.key\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/key\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    ValueTask\u003cstring?\u003e KeyAsync(double index);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.removeItem\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/removeItem\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    ValueTask RemoveItemAsync(string key);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.localStorage.setItem\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Storage/setItem\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    ValueTask SetItemAsync(string key, string value);\r\n}\r\n```\r\n\r\nNotice, that since the generic method descriptors are not added generics are not supported. This is not yet implemented as I've been focusing on WebAssembly scenarios.\r\n\r\n## Design goals 🎯\r\n\r\nI was hoping to use the [TypeScript lib.dom.d.ts](https://github.com/microsoft/TypeScript/blob/315b807489b8ff3a892179488fb0c00398d9b2c3/lib/lib.dom.d.ts) bits as input. This input would be read, parsed, and cached within the generator. The generator code would be capable of generating extension methods on the `IJSRuntime`. Additionally, the generator will create object graphs from the well know web APIs.\r\n\r\nUsing the _lib.dom.d.ts_ file, we could hypothetically parse various TypeScript type definitions. These definitions could then be converted to C# counterparts. While I realize that not all TypeScript is mappable to C#, there is a bit of room for interpretation.\r\n\r\nConsider the following type definition:\r\n\r\n```typescript\r\n/**\r\nAn object can programmatically obtain the position of the device.\r\nIt gives Web content access to the location of the device. This allows\r\na Web site or app to offer customized results based on the user's location.\r\n*/\r\ninterface Geolocation {\r\n\r\n    clearWatch(watchId: number): void;\r\n\r\n    getCurrentPosition(\r\n        successCallback: PositionCallback,\r\n        errorCallback?: PositionErrorCallback | null,\r\n        options?: PositionOptions): void;\r\n    \r\n    watchPosition(\r\n        successCallback: PositionCallback,\r\n        errorCallback?: PositionErrorCallback | null,\r\n        options?: PositionOptions): number;\r\n}\r\n```\r\n\r\n\u003e This is from the TypeScript repo, [lib.dom.d.ts file lines 5,498-5,502](https://github.com/microsoft/TypeScript/blob/315b807489b8ff3a892179488fb0c00398d9b2c3/lib/lib.dom.d.ts#L5497-L5502).\r\n\r\n### Example consumption of source generator ✔️\r\n\r\nIdeally, I would like to be able to define a C# class such as this:\r\n\r\n```csharp\r\n// Copyright (c) David Pine. All rights reserved.\r\n// Licensed under the MIT License.\r\n\r\nnamespace Microsoft.JSInterop;\r\n\r\n[JSAutoInterop(\r\n    TypeName = \"Geolocation\",\r\n    Implementation = \"window.navigator.geolocation\",\r\n    Url = \"https://developer.mozilla.org/docs/Web/API/Geolocation\")]\r\npublic partial interface IGeolocationService\r\n{\r\n}\r\n```\r\n\r\nThe source generator will expose the `JSAutoInteropAttribute`, and consuming libraries will decorate their classes with it. The generator code will see this class, and use the `TypeName` from the attribute to find the corresponding type to implement.\r\nWith the type name, the generator will generate the corresponding methods, and return types. The method implementations will be extensions of the `IJSRuntime`.\r\n\r\nThe following is an example resulting source generated `IGeolocationService` object:\r\n\r\n```csharp\r\nnamespace Microsoft.JSInterop;\r\n\r\npublic partial interface IGeolocationService\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.navigator.geolocation.clearWatch\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Geolocation/clearWatch\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    void ClearWatch(double watchId);\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.navigator.geolocation.getCurrentPosition\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Geolocation/getCurrentPosition\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    /// \u003cparam name=\"component\"\u003eThe calling Razor (or Blazor) component.\u003c/param\u003e\r\n    /// \u003cparam name=\"onSuccessCallbackMethodName\"\u003eExpects the name of a \u003cc\u003e\"JSInvokableAttribute\"\u003c/c\u003e C# method with the following \u003cc\u003eSystem.Action{GeolocationPosition}\"\u003c/c\u003e.\u003c/param\u003e\r\n    /// \u003cparam name=\"onErrorCallbackMethodName\"\u003eExpects the name of a \u003cc\u003e\"JSInvokableAttribute\"\u003c/c\u003e C# method with the following \u003cc\u003eSystem.Action{GeolocationPositionError}\"\u003c/c\u003e.\u003c/param\u003e\r\n    /// \u003cparam name=\"options\"\u003eThe \u003cc\u003ePositionOptions\u003c/c\u003e value.\u003c/param\u003e\r\n    void GetCurrentPosition\u003cTComponent\u003e(\r\n        TComponent component,\r\n        string onSuccessCallbackMethodName,\r\n        string? onErrorCallbackMethodName = null,\r\n        PositionOptions? options = null)\r\n        where TComponent : class;\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source generated implementation of \u003cc\u003ewindow.navigator.geolocation.watchPosition\u003c/c\u003e.\r\n    /// \u003ca href=\"https://developer.mozilla.org/docs/Web/API/Geolocation/watchPosition\"\u003e\u003c/a\u003e\r\n    /// \u003c/summary\u003e\r\n    /// \u003cparam name=\"component\"\u003eThe calling Razor (or Blazor) component.\u003c/param\u003e\r\n    /// \u003cparam name=\"onSuccessCallbackMethodName\"\u003eExpects the name of a \u003cc\u003e\"JSInvokableAttribute\"\u003c/c\u003e C# method with the following \u003cc\u003eSystem.Action{GeolocationPosition}\"\u003c/c\u003e.\u003c/param\u003e\r\n    /// \u003cparam name=\"onErrorCallbackMethodName\"\u003eExpects the name of a \u003cc\u003e\"JSInvokableAttribute\"\u003c/c\u003e C# method with the following \u003cc\u003eSystem.Action{GeolocationPositionError}\"\u003c/c\u003e.\u003c/param\u003e\r\n    /// \u003cparam name=\"options\"\u003eThe \u003cc\u003ePositionOptions\u003c/c\u003e value.\u003c/param\u003e\r\n    double WatchPosition\u003cTComponent\u003e(\r\n        TComponent component, \r\n        string onSuccessCallbackMethodName, \r\n        string? onErrorCallbackMethodName = null, \r\n        PositionOptions? options = null) \r\n        where TComponent : class;\r\n}\r\n\r\n```\r\n\r\nThe generator will also produce the corresponding APIs object types. For example, the Geolocation API defines the following:\r\n\r\n- `GeolocationService`\r\n- `PositionOptions`\r\n- `GeolocationCoordinates`\r\n- `GeolocationPosition`\r\n- `GeolocationPositionError`\r\n\r\n```csharp\r\nnamespace Microsoft.JSInterop;\r\n\r\n/// \u003cinheritdoc /\u003e\r\ninternal sealed class GeolocationService : IGeolocationService\r\n{\r\n    private readonly IJSInProcessRuntime _javaScript = null;\r\n\r\n    public GeolocationService(IJSInProcessRuntime javaScript)\r\n    {\r\n        _javaScript = javaScript;\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IGeolocationService.ClearWatch(System.Double)\" /\u003e\r\n    void IGeolocationService.ClearWatch(double watchId)\r\n    {\r\n        _javaScript.InvokeVoid(\"window.navigator.geolocation.clearWatch\", watchId);\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IGeolocationService.GetCurrentPosition``1(``0,System.String,System.String,Microsoft.JSInterop.PositionOptions)\" /\u003e\r\n    void IGeolocationService.GetCurrentPosition\u003cTComponent\u003e(\r\n        TComponent component, \r\n        string onSuccessCallbackMethodName, \r\n        string? onErrorCallbackMethodName, \r\n        PositionOptions? options)\r\n    {\r\n        _javaScript.InvokeVoid(\"blazorators.getCurrentPosition\", DotNetObjectReference.Create\u003cTComponent\u003e(component), onSuccessCallbackMethodName, onErrorCallbackMethodName, options);\r\n    }\r\n\r\n    /// \u003cinheritdoc cref=\"M:Microsoft.JSInterop.IGeolocationService.WatchPosition``1(``0,System.String,System.String,Microsoft.JSInterop.PositionOptions)\" /\u003e\r\n    double IGeolocationService.WatchPosition\u003cTComponent\u003e(\r\n        TComponent component, \r\n        string onSuccessCallbackMethodName, \r\n        string? onErrorCallbackMethodName, \r\n        PositionOptions? options)\r\n    {\r\n        return _javaScript.Invoke\u003cdouble\u003e(\"blazorators.watchPosition\", new object[4]\r\n        {\r\n            DotNetObjectReference.Create\u003cTComponent\u003e(component),\r\n            onSuccessCallbackMethodName,\r\n            onErrorCallbackMethodName,\r\n            options\r\n        });\r\n    }\r\n}\r\n```\r\n\r\n```csharp\r\nusing System.Text.Json.Serialization;\r\n\r\nnamespace Microsoft.JSInterop;\r\n\r\n/// \u003csummary\u003e\r\n/// Source-generated object representing an ideally immutable \u003cc\u003eGeolocationPosition\u003c/c\u003e value.\r\n/// \u003c/summary\u003e\r\npublic class GeolocationPosition\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPosition.coords\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"coords\")]\r\n    public GeolocationCoordinates Coords\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPosition.timestamp\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"timestamp\")]\r\n    public long Timestamp\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPosition.timestamp\u003c/c\u003e value, \r\n    /// converted as a \u003csee cref=\"T:System.DateTime\" /\u003e in UTC.\r\n    /// \u003c/summary\u003e\r\n    [JsonIgnore]\r\n    public DateTime TimestampAsUtcDateTime =\u003e Timestamp.ToDateTimeFromUnix();\r\n}\r\n\r\n/// \u003csummary\u003e\r\n/// Source-generated object representing an ideally immutable \u003cc\u003eGeolocationCoordinates\u003c/c\u003e value.\r\n/// \u003c/summary\u003e\r\npublic class GeolocationCoordinates\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.accuracy\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"accuracy\")]\r\n    public double Accuracy\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.altitude\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"altitude\")]\r\n    public double? Altitude\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.altitudeAccuracy\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"altitudeAccuracy\")]\r\n    public double? AltitudeAccuracy\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.heading\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"heading\")]\r\n    public double? Heading\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.latitude\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"latitude\")]\r\n    public double Latitude\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.longitude\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"longitude\")]\r\n    public double Longitude\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationCoordinates.speed\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"speed\")]\r\n    public double? Speed\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n}\r\n\r\n/// \u003csummary\u003e\r\n/// Source-generated object representing an ideally immutable \u003cc\u003eGeolocationPositionError\u003c/c\u003e value.\r\n/// \u003c/summary\u003e\r\npublic class GeolocationPositionError\r\n{\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPositionError.code\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"code\")]\r\n    public double Code\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPositionError.message\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"message\")]\r\n    public string Message\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPositionError.PERMISSION_DENIED\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"PERMISSION_DENIED\")]\r\n    public double PERMISSION_DENIED\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPositionError.POSITION_UNAVAILABLE\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"POSITION_UNAVAILABLE\")]\r\n    public double POSITION_UNAVAILABLE\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n\r\n    /// \u003csummary\u003e\r\n    /// Source-generated property representing the \u003cc\u003eGeolocationPositionError.TIMEOUT\u003c/c\u003e value.\r\n    /// \u003c/summary\u003e\r\n    [JsonPropertyName(\"TIMEOUT\")]\r\n    public double TIMEOUT\r\n    {\r\n        get;\r\n        set;\r\n    }\r\n}\r\n\r\n// Additional models omitted for brevity...\r\n```\r\n\r\nIn addition to this `GeolocationExtensions` class being generated, the generator will also generate a bit of JavaScript. Some methods cannot be directly invoked as they define callbacks. The approach the generator takes is to delegate callback methods on a given `T` instance, with the `JSInvokable` attribute. Our generator should also warn when the corresponding `T` instance doesn't define a matching method name that is also `JSInvokable`.\r\n\r\n```javascript\r\nconst getCurrentLocation =\r\n    (dotnetObj, successMethodName, errorMethodName, options) =\u003e\r\n    {\r\n        if (navigator \u0026\u0026 navigator.geolocation) {\r\n            navigator.geolocation.getCurrentPosition(\r\n                (position) =\u003e {\r\n                    dotnetObj.invokeMethodAsync(\r\n                        successMethodName, position);\r\n                },\r\n                (error) =\u003e {\r\n                    dotnetObj.invokeMethodAsync(\r\n                        errorMethodName, error);\r\n                },\r\n                options);\r\n        }\r\n    };\r\n\r\n// Other implementations omitted for brevity...\r\n// But we'd also define a \"watchPosition\" wrapper.\r\n// The \"clearWatch\" is a straight pass-thru, no wrapper needed.\r\n\r\nwindow.blazorator = {\r\n    getCurrentLocation,\r\n    watchPosition\r\n};\r\n```\r\n\r\nThe resulting JavaScript will have to be exposed to consuming projects. Additionally, consuming projects will need to adhere to extension method consumption semantics. When calling generated extension methods that require .NET object references of type `T`, the callback names should be marked with `JSInvokable` and the `nameof` operator should be used to ensure names are accurate. Consider the following example consuming Blazor component:\r\n\r\n```csharp\r\nusing Microsoft.AspNetCore.Components;\r\nusing Microsoft.JSInterop;\r\nusing Microsoft.JSInterop.Extensions;\r\n\r\nnamespace Example.Components;\r\n\r\n// This is the other half of ConsumingComponent.razor\r\npublic sealed partial class ConsumingComponent\r\n{\r\n    [Inject]\r\n    public IJSRuntime JavaScript { get; set; }\r\n\r\n    protected override async Task OnAfterRenderAsync(bool firstRender)\r\n    {\r\n        if (firstRender)\r\n        {\r\n            await JavaScript.GetCurrentPositionAsync(\r\n                this,\r\n                nameof(OnCoordinatesPermitted),\r\n                nameof(OnErrorRequestingCoordinates));\r\n        }\r\n    }\r\n\r\n    [JSInvokable]\r\n    public async Task OnCoordinatesPermitted(\r\n        GeolocationPosition position)\r\n    {\r\n        // TODO: consume/handle position.\r\n\r\n        await InvokeAsync(StateHasChanged);\r\n    }\r\n\r\n    [JSInvokable]\r\n    public async Task OnErrorRequestingCoordinates(\r\n        GeolocationPositionError error)\r\n    {\r\n        // TODO: consume/handle error.\r\n\r\n        await InvokeAsync(StateHasChanged);\r\n    }\r\n}\r\n```\r\n\r\n## Pseudocode and logical flow ➡️\r\n\r\n1. Consumer decorates a `static partial class` with the `JSAutoInteropAttribute`.\r\n1. Source generator is called:\r\n   - `JavaScriptInteropGenerator.Initialize`\r\n   - `JavaScriptInteropGenerator.Execute`\r\n1. The generator determines the `TypeName` from the attribute of the contextual class.\r\n   1. The `TypeName` is used to look up the corresponding TypeScript type definition.\r\n   1. If found, and a valid API - attempt source generation.\r\n\r\n\u003c!-- TODO: Add mermaid sequence diagram here --\u003e\r\n\r\n## Future work\r\n\r\n- https://developer.mozilla.org/docs/Web/API/CredentialsContainer\r\n- https://developer.mozilla.org/docs/Web/API/WakeLock\r\n- https://developer.mozilla.org/docs/Web/API/Navigator/hid\r\n- https://developer.mozilla.org/docs/Web/API/Web_Crypto_API\r\n\r\n## Known limitations ⚠️\r\n\r\nAt the time of writing, only pure JavaScript interop is supported. It is a stretch goal to add the following (currently missing) features:\r\n\r\n- Source generate corresponding (and supporting) JavaScript files.\r\n  - We'd need to accept a desired output path from the consumer, `JavaScriptOutputPath`.\r\n  - We would need to append all JavaScript into a single builder, and emit it collectively.\r\n- Allow for declarative and custom type mappings, for example; suppose the consumer wants the API to use generics instead of `string`.\r\n  - We'd need to expose a `TypeConverter` parameter and allow for consumers to implement their own.\r\n  - We'd provide a default one for standard JSON serialization, `StringTypeConverter` (maybe make this the default).\r\n\r\n## References and resources 📑\r\n\r\n- [MDN Web Docs: Web APIs](https://developer.mozilla.org/docs/Web/API)\r\n- [TypeScript DOM lib generator](https://github.com/microsoft/TypeScript-DOM-lib-generator)\r\n- [ASP.NET Core Docs: Blazor JavaScript interop](https://docs.microsoft.com/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet)\r\n- [Jared Parsons - GitHub Channel 9 Source Generators](https://github.com/jaredpar/channel9-source-generators)\r\n- [.NET Docs: C# Source Generators](https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/source-generators-overview)\r\n- [Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md)\r\n- [Source Generators: Design Document](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.md)\r\n\r\n## Contributors ✨\r\n\r\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\r\n\r\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://www.cnblogs.com/weihanli\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/7604648?v=4?s=100\" width=\"100px;\" alt=\"Weihan Li\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eWeihan Li\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=WeihanLi\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://www.microsoft.com\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/7679720?v=4?s=100\" width=\"100px;\" alt=\"David Pine\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eDavid Pine\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=IEvangelist\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"#design-IEvangelist\" title=\"Design\"\u003e🎨\u003c/a\u003e \u003ca href=\"https://github.com/IEvangelist/blazorators/pulls?q=is%3Apr+reviewed-by%3AIEvangelist\" title=\"Reviewed Pull Requests\"\u003e👀\u003c/a\u003e \u003ca href=\"#ideas-IEvangelist\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e \u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=IEvangelist\" title=\"Tests\"\u003e⚠️\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://nimbleapps.cloud\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/1657085?v=4?s=100\" width=\"100px;\" alt=\"Robert McLaws\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eRobert McLaws\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=robertmclaws\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/IEvangelist/blazorators/issues?q=author%3Arobertmclaws\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e \u003ca href=\"#ideas-robertmclaws\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"http://colinsalmcorner.com\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/1932561?v=4?s=100\" width=\"100px;\" alt=\"Colin Dembovsky\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eColin Dembovsky\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"#infra-colindembovsky\" title=\"Infrastructure (Hosting, Build-Tools, etc)\"\u003e🚇\u003c/a\u003e \u003ca href=\"#platform-colindembovsky\" title=\"Packaging/porting to new platform\"\u003e📦\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"http://tanayparikh.com\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/14852843?v=4?s=100\" width=\"100px;\" alt=\"Tanay Parikh\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eTanay Parikh\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=TanayParikh\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/taori\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/5545184?v=4?s=100\" width=\"100px;\" alt=\"Andreas Müller\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAndreas Müller\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/issues?q=author%3Ataori\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e \u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=taori\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://www.mahmudx.com\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/16564582?v=4?s=100\" width=\"100px;\" alt=\"Mahmudul Hasan\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eMahmudul Hasan\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=MahmudX\" title=\"Code\"\u003e💻\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/fabiansanchez18\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/106093861?v=4?s=100\" width=\"100px;\" alt=\"fabiansanchez18\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003efabiansanchez18\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/issues?q=author%3Afabiansanchez18\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"http://weblogs.asp.net/sfeldman\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/1309622?v=4?s=100\" width=\"100px;\" alt=\"Sean Feldman\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eSean Feldman\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/issues?q=author%3ASeanFeldman\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/daver77\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/2369739?v=4?s=100\" width=\"100px;\" alt=\"daver77\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003edaver77\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"#ideas-daver77\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\" valign=\"top\" width=\"14.28%\"\u003e\u003ca href=\"https://github.com/Denny09310\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/50493437?v=4?s=100\" width=\"100px;\" alt=\"Denny09310\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eDenny09310\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=Denny09310\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/IEvangelist/blazorators/commits?author=Denny09310\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"#ideas-Denny09310\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\r\n\r\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome!\r\n","funding_links":["https://github.com/sponsors/IEvangelist"],"categories":["Content","typescript","hacktoberfest"],"sub_categories":["102. [Blazorators](https://ignatandrei.github.io/RSCG_Examples/v2/docs/Blazorators) , in the [Blazor](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#blazor) category"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FIEvangelist%2Fblazorators","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FIEvangelist%2Fblazorators","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FIEvangelist%2Fblazorators/lists"}