├── ICON.png ├── README_IMAGE.png ├── Blazor.TSRuntime.slnx ├── Blazor.TSRuntime ├── Configs │ ├── NamePattern │ │ ├── Utils │ │ │ ├── Output.cs │ │ │ ├── OutputBlock.cs │ │ │ └── NameTransform.cs │ │ ├── ModuleNamePattern.cs │ │ └── FunctionNamePattern.cs │ └── Types │ │ ├── GenericType.cs │ │ ├── MappedType.cs │ │ └── InputPath.cs ├── StringBuilderInterpolation.cs ├── Parsing │ ├── TSFile │ │ ├── TSScript.cs │ │ ├── TSModule.cs │ │ └── TSFile.cs │ └── TSParameter.cs ├── Generation │ └── ServiceExtensionBuilder.cs ├── Blazor.TSRuntime.csproj ├── TSRuntimeGenerator.cs └── DiagnosticErrors.cs ├── Readme_md ├── JSRuntime.md ├── UsingStatements.md ├── PromiseFunction.md ├── ModuleGrouping.md ├── NamePattern.md ├── InputPath.md └── TypeMap.md ├── PACKAGE.md ├── Blazor.TSRuntime.Tests ├── GeneratorTests │ ├── AcceptAllTests.cs │ ├── ScriptTests │ │ ├── ScriptTests.PromiseFunction.verified.txt │ │ ├── ScriptTests.PromiseReturnFunction.verified.txt │ │ ├── ScriptTests.ParameterlessFunction.verified.txt │ │ ├── ScriptTests.ParameterAndReturnTypeFunction.verified.txt │ │ └── ScriptTests.cs │ ├── SummaryTests │ │ ├── GeneratorSummaryTests.ReturnsOnly.verified.txt │ │ ├── GeneratorSummaryTests.ReturnsOnly_JSDoc.verified.txt │ │ ├── GeneratorSummaryTests.SummaryOnly.verified.txt │ │ ├── GeneratorSummaryTests.RemarksOnly.verified.txt │ │ ├── GeneratorSummaryTests.ParamOnly.verified.txt │ │ ├── GeneratorSummaryTests.ParamOnly_JSDoc.verified.txt │ │ ├── GeneratorSummaryTests.SummaryAndRemarksAndParamAndReturns.verified.txt │ │ ├── GeneratorSummaryTests.SummaryAndRemarksAndParamAndReturns_JSDocs.verified.txt │ │ └── GeneratorSummaryTests.cs │ ├── ConfigTests │ │ ├── GeneratorConfigTests.TypeMap_MapsIdentity.verified.txt │ │ ├── GeneratorConfigTests.TypeMap_MapsGeneric.verified.txt │ │ ├── GeneratorConfigTests.TypeMap_MapsArray.verified.txt │ │ ├── GeneratorConfigTests.TypeMap_MapsNullable.verified.txt │ │ ├── GeneratorConfigTests.TypeMap_MapsNullableArray.verified.txt │ │ ├── GeneratorConfigTests.TypeMap_MapsNullableArrayWithNullableItems.verified.txt │ │ ├── GeneratorConfigTests.TypeMap_MapsMultipleGenerics.verified.txt │ │ └── GeneratorConfigTests.TypeMap_MapsGenericExceptOptionalIsNotIncluded.verified.txt │ ├── GenericsTests │ │ ├── GeneratorGenericsTests.cs │ │ └── GeneratorGenericsTests.JSGenerics.verified.txt │ └── CallbackTests │ │ └── GeneratorCallbackTests.cs ├── Blazor.TSRuntime.Tests.csproj ├── GenerateSourceTextExtension.cs └── InputPathTests.cs ├── LICENSE ├── .gitignore └── README.md /ICON.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackWhiteYoshi/Blazor.TSRuntime/HEAD/ICON.png -------------------------------------------------------------------------------- /README_IMAGE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackWhiteYoshi/Blazor.TSRuntime/HEAD/README_IMAGE.png -------------------------------------------------------------------------------- /Blazor.TSRuntime.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/NamePattern/Utils/Output.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Configs.NamePattern; 2 | 3 | internal enum Output { 4 | Function, 5 | Module, 6 | Action, 7 | String 8 | } 9 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/Types/GenericType.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Configs; 2 | 3 | public readonly record struct GenericType(string name) { 4 | public string Name { get; init; } = name; 5 | public string? Constraint { get; init; } = null; 6 | } 7 | -------------------------------------------------------------------------------- /Readme_md/JSRuntime.md: -------------------------------------------------------------------------------- 1 | # Config - JS Runtime 2 | 3 | It exposes the JSRuntime functionalities, so you can use the generic IJSRuntime methods with the ITSRuntime interface. 4 | Additionally, the InvokeTrySync-method is also available, which does not exist in the IJSRuntime interface. 5 | -------------------------------------------------------------------------------- /PACKAGE.md: -------------------------------------------------------------------------------- 1 | # Blazor.TSRuntime 2 | 3 | An improved JSRuntime with 4 | 5 | - automatic JS-module loading and caching 6 | - compile time errors instead of runtime errors 7 | - IntelliSense guidance 8 | 9 | For documentation or sourcecode see [github.com/BlackWhiteYoshi/Blazor.TSRuntime](https://github.com/BlackWhiteYoshi/Blazor.TSRuntime). 10 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/NamePattern/Utils/OutputBlock.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Configs.NamePattern; 2 | 3 | internal readonly record struct OutputBlock(Output Output, string Content) { 4 | public static implicit operator OutputBlock(Output output) => new(output, string.Empty); 5 | 6 | public static implicit operator OutputBlock(string content) => new(Output.String, content); 7 | } 8 | -------------------------------------------------------------------------------- /Readme_md/UsingStatements.md: -------------------------------------------------------------------------------- 1 | # Config - Using Statements 2 | 3 | The following using statements are always included 4 | 5 | - using System.Threading; 6 | - using System.Threading.Tasks; 7 | 8 | The values given in \[using statements\] will add additional using statements. 9 | 10 | Alternative you can also fully qualify your types in [TypeMap](TypeMap.md) and leave this empty. 11 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/AcceptAllTests.cs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env dotnet 2 | 3 | /** 4 | * goes throgh the folder and subfolders 5 | * if file name is "*.received.txt" 6 | * rename file to "*.verified.txt" (and overwrite existing file if any) 7 | **/ 8 | 9 | foreach (string fileName in Directory.GetFiles(Directory.GetCurrentDirectory(), "*", SearchOption.AllDirectories)) 10 | if (fileName.EndsWith(".received.txt")) { 11 | string baseName = fileName[..^".received.txt".Length]; 12 | File.Move(fileName, $"{baseName}.verified.txt", overwrite: true); 13 | Console.WriteLine($"accepted {Path.GetFileName(baseName)}"); 14 | } 15 | -------------------------------------------------------------------------------- /Readme_md/PromiseFunction.md: -------------------------------------------------------------------------------- 1 | # Config - Promise Function 2 | 3 | **[only async enabled]**: Whenever a module function returns a promise, the *[invoke function].[sync enabled]*, *[invoke function].[trysync enabled]* and *[invoke function].[async enabled]* flags will be ignored 4 | and instead, only the async invoke method will be generated. 5 | Asynchronous JS-functions will only be awaited with the async invoke method, so this value should always be true. 6 | Set it only to false when you know what you are doing. 7 | 8 | **[append async]**: Whenever a module function returns a promise, the string "Async" is appended. 9 | If your pattern ends already with "Async", for example with the #action# variable, this will result in a double "AsyncAsync". 10 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/Blazor.TSRuntime.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ScriptTests/ScriptTests.PromiseFunction.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | /// 15 | /// Invokes in script 'site' the JS-function 'Test' asynchronously. 16 | /// 17 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 18 | /// A Task that will complete when the JS-Function have completed. 19 | public async ValueTask TestInvokeAsync(CancellationToken cancellationToken = default) { 20 | await TSInvokeAsync("Test", [], cancellationToken); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 BlackWhiteYoshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ScriptTests/ScriptTests.PromiseReturnFunction.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | /// 15 | /// Invokes in script 'site' the JS-function 'Test' asynchronously. 16 | /// 17 | /// 18 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 19 | /// Result of the JS-function. 20 | public async ValueTask TestInvokeAsync(CancellationToken cancellationToken = default) where TNumber : INumber { 21 | return await TSInvokeAsync("Test", [], cancellationToken); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/StringBuilderInterpolation.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace TSRuntime.Generation; 5 | 6 | public static class StringBuilderInterpolation { 7 | /// 8 | /// The same as , but only for interpolated strings: $"..."
9 | /// It constructs the string directly in the builder, so no unnecessary string memory allocations. 10 | ///
11 | /// 12 | /// 13 | /// 14 | public static StringBuilder AppendInterpolation(this StringBuilder builder, [InterpolatedStringHandlerArgument("builder")] StringBuilderInterpolationHandler handler) => builder; 15 | 16 | [InterpolatedStringHandler] 17 | public readonly ref struct StringBuilderInterpolationHandler { 18 | private readonly StringBuilder builder; 19 | 20 | public StringBuilderInterpolationHandler(int literalLength, int formattedCount, StringBuilder builder) => this.builder = builder; 21 | 22 | public void AppendLiteral(string str) => builder.Append(str); 23 | 24 | public void AppendFormatted(T item) => builder.Append(item); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Parsing/TSFile/TSScript.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace TSRuntime.Parsing; 4 | 5 | /// 6 | /// Represents a js-script (a js-file placed in html). 7 | /// 8 | public sealed class TSScript : TSFile { 9 | /// 10 | /// Creates an object with , and filled and an empty . 11 | /// 12 | /// 13 | /// 14 | public TSScript(string filePath, List errorList) { 15 | FilePath = filePath; 16 | 17 | // ModulePath 18 | ReadOnlySpan path = filePath.AsSpan(); 19 | URLPath = CreateURLPath(ref path); 20 | 21 | Name = CreateModuleName(path); 22 | } 23 | 24 | /// 25 | /// Creates an object with FunctionList. 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | public TSScript(string filePath, string urlPath, string name, IReadOnlyList functionList) => (FilePath, URLPath, Name, FunctionList) = (filePath, urlPath, name, functionList); 32 | } 33 | -------------------------------------------------------------------------------- /Readme_md/ModuleGrouping.md: -------------------------------------------------------------------------------- 1 | # Config - Module Grouping And Service Extension 2 | 3 | ## Module Grouping 4 | 5 | When your ITSRuntime interface gets big and complex, you can split it up into multiple interfaces, each represents a module. 6 | To enable module grouping you can use a shorthand and set the \[module grouping] key directly to true: 7 | 8 | ```json 9 | { 10 | "module grouping": true 11 | 12 | // - the same as 13 | // "module grouping": { 14 | // "enabled": true, 15 | // "interface name pattern": { 16 | // "pattern": "I#module#Module", 17 | // "module transform": "first upper case" 18 | // } 19 | // } 20 | } 21 | ``` 22 | 23 | This will result in setting \[module grouping].[enabled] = true, while \[module grouping].[interface name pattern] will have its default value. 24 | 25 | With [interface name pattern] you can specify the naming of your module interfaces. For how it works see [Name Pattern]. 26 | 27 | 28 | ## Service Extension 29 | 30 | When [service extension] is enabled, you can use the generated extension method to register all generated module interfaces to your service collection. 31 | This will register a scoped ITSRuntime with a TSRuntime instance as implementation and registers the module interfaces with the same TSRuntime-instance. 32 | If module grouping is disabled and service extension is enabled, it will only register ITSRuntime as scoped dependency with a TSRuntime instance as implementation. 33 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/NamePattern/Utils/NameTransform.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Configs.NamePattern; 2 | 3 | /// 4 | /// Transforms a placeholder in the name pattern. 5 | /// 6 | public enum NameTransform { 7 | /// 8 | /// No Transform. 9 | /// 10 | None, 11 | 12 | /// 13 | /// Changes the first letter to uppercase. 14 | /// 15 | FirstUpperCase, 16 | 17 | /// 18 | /// Changes the first letter to lowercase. 19 | /// 20 | FirstLowerCase, 21 | 22 | /// 23 | /// Changes all letters to uppercase. 24 | /// 25 | UpperCase, 26 | 27 | /// 28 | /// Changes all letters to lowercase. 29 | /// 30 | LowerCase 31 | } 32 | 33 | internal static class NameTransformExtension { 34 | internal static string Transform(this NameTransform transform, string name) { 35 | if (name.Length == 0) 36 | return string.Empty; 37 | 38 | return transform switch { 39 | NameTransform.None => name, 40 | NameTransform.UpperCase => name.ToUpper(), 41 | NameTransform.LowerCase => name.ToLower(), 42 | NameTransform.FirstUpperCase => $"{char.ToUpperInvariant(name[0])}{name[1..]}", 43 | NameTransform.FirstLowerCase => $"{char.ToLowerInvariant(name[0])}{name[1..]}", 44 | _ => throw new ArgumentException("Not Reachable: Invalid Enum 'NameTransform'") 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.ReturnsOnly.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 26 | /// 27 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 28 | /// a string 29 | public async ValueTask Test(CancellationToken cancellationToken = default) { 30 | return await TSInvokeTrySync(GetmoduleModule(), "test", [], cancellationToken); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.ReturnsOnly_JSDoc.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 26 | /// 27 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 28 | /// a string 29 | public async ValueTask Test(CancellationToken cancellationToken = default) { 30 | return await TSInvokeTrySync(GetmoduleModule(), "test", [], cancellationToken); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.SummaryOnly.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// The example Summary 26 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 27 | /// 28 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 29 | /// A Task that will complete when the JS-Function have completed. 30 | public async ValueTask Test(CancellationToken cancellationToken = default) { 31 | await TSInvokeTrySync(GetmoduleModule(), "test", [], cancellationToken); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.RemarksOnly.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 26 | /// 27 | /// The example remark 28 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 29 | /// A Task that will complete when the JS-Function have completed. 30 | public async ValueTask Test(CancellationToken cancellationToken = default) { 31 | await TSInvokeTrySync(GetmoduleModule(), "test", [], cancellationToken); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/Types/MappedType.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Configs; 2 | 3 | public readonly struct MappedType(string type, GenericType[] genericTypes) : IEquatable { 4 | public readonly string Type { get; init; } = type; 5 | public readonly GenericType[] GenericTypes { get; init; } = genericTypes; 6 | 7 | public MappedType(string type) : this(type, []) { } 8 | 9 | public MappedType(string type, string genericType) : this(type, [new GenericType(genericType)]) { } 10 | 11 | public MappedType(string type, GenericType genericType) : this(type, [genericType]) { } 12 | 13 | 14 | #region IEquatable 15 | 16 | public static bool operator ==(MappedType left, MappedType right) => left.Equals(right); 17 | 18 | public static bool operator !=(MappedType left, MappedType right) => !left.Equals(right); 19 | 20 | public override bool Equals(object obj) 21 | => obj switch { 22 | MappedType other => Equals(other), 23 | _ => false 24 | }; 25 | 26 | public bool Equals(MappedType other) { 27 | if (Type != other.Type) 28 | return false; 29 | 30 | if (!GenericTypes.SequenceEqual(other.GenericTypes)) 31 | return false; 32 | 33 | return true; 34 | } 35 | 36 | public override int GetHashCode() { 37 | int hash = Type.GetHashCode(); 38 | 39 | foreach (GenericType genericType in GenericTypes) 40 | hash = Combine(hash, genericType.GetHashCode()); 41 | 42 | return hash; 43 | 44 | 45 | static int Combine(int h1, int h2) { 46 | uint r = (uint)h1 << 5 | (uint)h1 >> 27; 47 | return (int)r + h1 ^ h2; 48 | } 49 | } 50 | 51 | #endregion 52 | } 53 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsIdentity.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetTestModuleModule(); 19 | 20 | /// 21 | /// Loads 'TestModule' (/TestModule.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadTestModule() => GetTestModuleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'TestModule' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 34 | /// Result of the JS-function. 35 | public async ValueTask Test(number a, string b, CancellationToken cancellationToken = default) { 36 | return await TSInvokeTrySync(GetTestModuleModule(), "Test", [a, b], cancellationToken); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.ParamOnly.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 26 | /// 27 | /// 28 | /// a is not B 29 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 30 | /// A Task that will complete when the JS-Function have completed. 31 | public async ValueTask Test(TNumber a, CancellationToken cancellationToken = default) where TNumber : INumber { 32 | await TSInvokeTrySync(GetmoduleModule(), "test", [a], cancellationToken); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.ParamOnly_JSDoc.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 26 | /// 27 | /// 28 | /// a is not B 29 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 30 | /// A Task that will complete when the JS-Function have completed. 31 | public async ValueTask Test(TNumber a, CancellationToken cancellationToken = default) where TNumber : INumber { 32 | await TSInvokeTrySync(GetmoduleModule(), "test", [a], cancellationToken); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsGeneric.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetTestModuleModule(); 19 | 20 | /// 21 | /// Loads 'TestModule' (/TestModule.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadTestModule() => GetTestModuleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'TestModule' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 35 | /// Result of the JS-function. 36 | public async ValueTask Test(TN a, string b, CancellationToken cancellationToken = default) { 37 | return await TSInvokeTrySync(GetTestModuleModule(), "Test", [a, b], cancellationToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsArray.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetmoduleModule(); 19 | 20 | /// 21 | /// Loads 'module' (/module.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadModule() => GetmoduleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'module' the JS-function 'TT' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 35 | /// Result of the JS-function. 36 | public async ValueTask TT(TNumber[] a, string[] b, CancellationToken cancellationToken = default) where TNumber : INumber { 37 | return await TSInvokeTrySync(GetmoduleModule(), "TT", [a, b], cancellationToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsNullable.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetmoduleModule(); 19 | 20 | /// 21 | /// Loads 'module' (/module.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadModule() => GetmoduleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'module' the JS-function 'TT' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 35 | /// Result of the JS-function. 36 | public async ValueTask TT(TNumber? a, string? b, CancellationToken cancellationToken = default) where TNumber : INumber { 37 | return await TSInvokeTrySync(GetmoduleModule(), "TT", [a, b], cancellationToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsNullableArray.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetmoduleModule(); 19 | 20 | /// 21 | /// Loads 'module' (/module.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadModule() => GetmoduleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'module' the JS-function 'TT' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 35 | /// Result of the JS-function. 36 | public async ValueTask TT(TNumber[]? a, string[]? b, CancellationToken cancellationToken = default) where TNumber : INumber { 37 | return await TSInvokeTrySync(GetmoduleModule(), "TT", [a, b], cancellationToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.SummaryAndRemarksAndParamAndReturns.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// The example Summary 26 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 27 | /// 28 | /// The example remark 29 | /// 30 | /// a is not B 31 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 32 | /// a string 33 | public async ValueTask Test(TNumber a, CancellationToken cancellationToken = default) where TNumber : INumber { 34 | return await TSInvokeTrySync(GetmoduleModule(), "test", [a], cancellationToken); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.SummaryAndRemarksAndParamAndReturns_JSDocs.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | protected Task GetmoduleModule(); 15 | 16 | /// 17 | /// Loads 'module' (/module.js) as javascript-module. 18 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 19 | /// 20 | /// A Task that will complete when the module import have completed. 21 | public Task PreloadModule() => GetmoduleModule(); 22 | 23 | 24 | /// 25 | /// The example Summary 26 | /// Invokes in module 'module' the JS-function 'test' synchronously when supported, otherwise asynchronously. 27 | /// 28 | /// The example remark 29 | /// 30 | /// a is not B 31 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 32 | /// a string 33 | public async ValueTask Test(TNumber a, CancellationToken cancellationToken = default) where TNumber : INumber { 34 | return await TSInvokeTrySync(GetmoduleModule(), "test", [a], cancellationToken); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsNullableArrayWithNullableItems.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetmoduleModule(); 19 | 20 | /// 21 | /// Loads 'module' (/module.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadModule() => GetmoduleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'module' the JS-function 'TT' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 35 | /// Result of the JS-function. 36 | public async ValueTask TT(TNumber?[]? a, string?[]? b, CancellationToken cancellationToken = default) where TNumber : INumber { 37 | return await TSInvokeTrySync(GetmoduleModule(), "TT", [a, b], cancellationToken); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsMultipleGenerics.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetTestModuleModule(); 19 | 20 | /// 21 | /// Loads 'TestModule' (/TestModule.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadTestModule() => GetTestModuleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'TestModule' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 36 | /// Result of the JS-function. 37 | public async ValueTask> Test(Dictionary a, string b, CancellationToken cancellationToken = default) { 38 | return await TSInvokeTrySync>(GetTestModuleModule(), "Test", [a, b], cancellationToken); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Readme_md/NamePattern.md: -------------------------------------------------------------------------------- 1 | # Config - Name Pattern 2 | 3 | \[name pattern\] describes the naming of the generated methods. 4 | For example, if you provide for the key [pattern] the value "MyMethod", all generated methods will have the name "MyMethod", which will result in a compile error. 5 | That is why there are variables provided to customize your method-naming. For the invoke methods there are 3 variables: 6 | 7 | - #module# 8 | - #function# 9 | - #action# 10 | 11 |

12 | Let's say we have a module named "Example" and a function "saveNumber": 13 | 14 | - "pattern": "#function##Example##action#": 15 | -> saveNumberExampleInvoke(...) 16 | -> saveNumberExampleInvokeTrySync(...) 17 | -> saveNumberExampleInvokeAsync(...) 18 | 19 | - "pattern": "#action#_text#function#": 20 | -> Invoke_textsaveNumber(...) 21 | -> InvokeTrySync_textsaveNumber(...) 22 | -> InvokeAsync_textsaveNumber(...) 23 | 24 |

25 | Like in the example JS-functions are normally lower case and in C# most things are upper case. 26 | To handle that you can apply lower/upper case transformation for each variable. 27 | NameTransform can be one of 5 different values: 28 | 29 | - **"none"**: identity, changes nothing 30 | - **"first upper case"**: first letter is uppercase 31 | - **"first lower case"**: first letter is lowercase 32 | - **"upper case"**: all letters are uppercase 33 | - **"lower case"**: all letters are lowercase 34 | 35 | With [function transform] set to "first upper case" you get: 36 | 37 | - "pattern": "#function##Example##action#": 38 | -> SaveNumberExampleInvoke(...) 39 | -> SaveNumberExampleInvokeTrySync(...) 40 | -> SaveNumberExampleInvokeAsync(...) 41 | 42 | - "pattern": "#action#_text#function#": 43 | -> Invoke_textSaveNumber(...) 44 | -> InvokeTrySync_textSaveNumber(...) 45 | -> InvokeAsync_textSaveNumber(...) 46 | 47 |

48 | The \[name pattern\] for preload or module grouping works pretty much the same, except there is only 1 variable: 49 | 50 | - #module# 51 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Parsing/TSFile/TSModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace TSRuntime.Parsing; 4 | 5 | /// 6 | /// Represents a js-module (a js-file loaded as module). 7 | /// 8 | public sealed class TSModule : TSFile { 9 | /// 10 | /// Creates an object with , and filled and an empty . 11 | /// 12 | /// 13 | /// 14 | /// 15 | public TSModule(string filePath, string? modulePath, List errorList) { 16 | FilePath = filePath; 17 | 18 | // ModulePath 19 | ReadOnlySpan path; 20 | if (modulePath == null) { 21 | path = filePath.AsSpan(); 22 | URLPath = CreateURLPath(ref path); 23 | } 24 | else { 25 | int startIndex; 26 | if (modulePath is ['/', ..]) { 27 | URLPath = modulePath; 28 | startIndex = 1; 29 | } 30 | else { 31 | URLPath = $"/{modulePath}"; 32 | startIndex = 0; 33 | } 34 | int extensionIndex = modulePath.LastIndexOf('.'); 35 | if (extensionIndex != -1) 36 | path = modulePath.AsSpan(startIndex, extensionIndex - startIndex); 37 | else 38 | path = modulePath.AsSpan(startIndex); 39 | } 40 | 41 | Name = CreateModuleName(path); 42 | } 43 | 44 | /// 45 | /// Creates an object with FunctionList. 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | public TSModule(string filePath, string urlPath, string name, IReadOnlyList functionList) => (FilePath, URLPath, Name, FunctionList) = (filePath, urlPath, name, functionList); 52 | } 53 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ScriptTests/ScriptTests.ParameterlessFunction.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | /// 15 | /// Invokes in script 'site' the JS-function 'Test' synchronously. 16 | /// 17 | public void TestInvoke() { 18 | TSInvoke("Test", []); 19 | } 20 | 21 | /// 22 | /// Invokes in script 'site' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 23 | /// 24 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 25 | /// A Task that will complete when the JS-Function have completed. 26 | public async ValueTask TestInvokeTrySync(CancellationToken cancellationToken = default) { 27 | await TSInvokeTrySync("Test", [], cancellationToken); 28 | } 29 | 30 | /// 31 | /// Invokes in script 'site' the JS-function 'Test' asynchronously. 32 | /// 33 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 34 | /// A Task that will complete when the JS-Function have completed. 35 | public async ValueTask TestInvokeAsync(CancellationToken cancellationToken = default) { 36 | await TSInvokeAsync("Test", [], cancellationToken); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ScriptTests/ScriptTests.ParameterAndReturnTypeFunction.verified.txt: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable 3 | #nullable enable annotations 4 | 5 | 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Components; 9 | using System.Numerics; 10 | 11 | namespace Microsoft.JSInterop; 12 | 13 | public partial interface ITSRuntime { 14 | /// 15 | /// Invokes in script 'site' the JS-function 'Test' synchronously. 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// Result of the JS-function. 21 | public TNumber TestInvoke(string str, bool a) where TNumber : INumber { 22 | return TSInvoke("Test", [str, a]); 23 | } 24 | 25 | /// 26 | /// Invokes in script 'site' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 32 | /// Result of the JS-function. 33 | public async ValueTask TestInvokeTrySync(string str, bool a, CancellationToken cancellationToken = default) where TNumber : INumber { 34 | return await TSInvokeTrySync("Test", [str, a], cancellationToken); 35 | } 36 | 37 | /// 38 | /// Invokes in script 'site' the JS-function 'Test' asynchronously. 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 44 | /// Result of the JS-function. 45 | public async ValueTask TestInvokeAsync(string str, bool a, CancellationToken cancellationToken = default) where TNumber : INumber { 46 | return await TSInvokeAsync("Test", [str, a], cancellationToken); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Generation/ServiceExtensionBuilder.cs: -------------------------------------------------------------------------------- 1 | using AssemblyVersionInfo; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.Extensions.ObjectPool; 4 | using System.Collections.Immutable; 5 | using System.Text; 6 | using TSRuntime.Configs; 7 | using TSRuntime.Parsing; 8 | 9 | namespace TSRuntime.Generation; 10 | 11 | /// 12 | /// Builds the extension method for IServiceCollection for registering TSRuntime. 13 | /// 14 | public static class ServiceExtensionBuilder { 15 | /// 16 | /// Builds the extension method for IServiceCollection for registering TSRuntime. 17 | /// 18 | /// 19 | /// 20 | /// 21 | public static void BuildServiceExtension(this ObjectPool stringBuilderPool, SourceProductionContext context, (ImmutableArray moduleList, (Config? config, Diagnostic? error) configOrError) parameters) { 22 | if (parameters.configOrError.error is not null) 23 | return; 24 | 25 | Config config = parameters.configOrError.config!; 26 | ImmutableArray moduleList = parameters.moduleList; 27 | 28 | if (!config.ServiceExtension) 29 | return; 30 | 31 | 32 | StringBuilder builder = stringBuilderPool.Get(); 33 | 34 | builder.Append($$""" 35 | // 36 | #pragma warning disable 37 | #nullable enable annotations 38 | 39 | 40 | using Microsoft.Extensions.DependencyInjection; 41 | 42 | namespace Microsoft.JSInterop; 43 | 44 | [System.CodeDom.Compiler.GeneratedCodeAttribute("{{Assembly.NAME}}", "{{Assembly.VERSION_MAJOR_MINOR_BUILD}}")] 45 | public static class TSRuntimeServiceExtension { 46 | /// 47 | /// Registers a scoped ITSRuntime with a TSRuntime as implementation and if available, registers the module interfaces with the same TSRuntime-object. 48 | /// 49 | /// 50 | /// 51 | public static IServiceCollection AddTSRuntime(this IServiceCollection services) { 52 | services.AddScoped(); 53 | 54 | 55 | """); 56 | 57 | if (config.ModuleGrouping) 58 | foreach (TSModule module in moduleList) { 59 | builder.Append(" services.AddScoped(serviceProvider => ("); 60 | config.ModuleGroupingNamePattern.AppendNaming(builder, module.Name); 61 | builder.Append(")serviceProvider.GetRequiredService());\n"); 62 | } 63 | else 64 | builder.Length--; 65 | 66 | builder.Append(""" 67 | 68 | return services; 69 | } 70 | } 71 | 72 | """); 73 | 74 | 75 | string source = builder.ToString(); 76 | context.AddSource("TSRuntime_ServiceExtension.g.cs", source); 77 | stringBuilderPool.Return(builder); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ConfigTests/GeneratorConfigTests.TypeMap_MapsGenericExceptOptionalIsNotIncluded.verified.txt: -------------------------------------------------------------------------------- 1 | ------ 2 | Module 3 | ------ 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using Microsoft.AspNetCore.Components; 13 | using System.Numerics; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | public partial interface ITSRuntime { 18 | protected Task GetmoduleModule(); 19 | 20 | /// 21 | /// Loads 'module' (/module.js) as javascript-module. 22 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 23 | /// 24 | /// A Task that will complete when the module import have completed. 25 | public Task PreloadModule() => GetmoduleModule(); 26 | 27 | 28 | /// 29 | /// Invokes in module 'module' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 35 | /// A Task that will complete when the JS-Function have completed. 36 | public async ValueTask Test(string a, TNumber b, CancellationToken cancellationToken = default) where TNumber : INumber { 37 | await TSInvokeTrySync(GetmoduleModule(), "Test", [a, b], cancellationToken); 38 | } 39 | 40 | /// 41 | /// Invokes in module 'module' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 42 | /// 43 | /// 44 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 45 | /// A Task that will complete when the JS-Function have completed. 46 | public async ValueTask Test(string a, CancellationToken cancellationToken = default) { 47 | await TSInvokeTrySync(GetmoduleModule(), "Test", [a], cancellationToken); 48 | } 49 | 50 | /// 51 | /// Invokes in module 'module' the JS-function 'Test' synchronously when supported, otherwise asynchronously. 52 | /// 53 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 54 | /// A Task that will complete when the JS-Function have completed. 55 | public async ValueTask Test(CancellationToken cancellationToken = default) { 56 | await TSInvokeTrySync(GetmoduleModule(), "Test", [], cancellationToken); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/GenericsTests/GeneratorGenericsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Collections.Immutable; 3 | 4 | namespace TSRuntime.Tests; 5 | 6 | public sealed class GeneratorGenericsTests { 7 | [Test] 8 | public async ValueTask JSGenerics() { 9 | const string jsonConfig = """{}"""; 10 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/GenericModule.d.ts", "export function generic(): A;\n"); 11 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 12 | await Assert.That(diagnostics).IsEmpty(); 13 | 14 | string tsRuntime = result[0]; 15 | string itsRuntimeCore = result[1]; 16 | string itsRuntimeModule = result[2]; 17 | await Assert.That(result.Length).IsEqualTo(4); 18 | await Verify($""" 19 | --------- 20 | TSRuntime 21 | --------- 22 | 23 | {tsRuntime.XVersionNumber()} 24 | 25 | ---------- 26 | ITSRuntime 27 | ---------- 28 | 29 | {itsRuntimeCore.XVersionNumber()} 30 | 31 | ------ 32 | Module 33 | ------ 34 | 35 | {itsRuntimeModule} 36 | """); 37 | } 38 | 39 | [Test] 40 | public async ValueTask JSGenericsConstraint() { 41 | const string jsonConfig = """{}"""; 42 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/GenericModule.d.ts", "export function genericKeyofConstraint(): void;\n"); 43 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 44 | await Assert.That(diagnostics).IsEmpty(); 45 | 46 | string tsRuntime = result[0]; 47 | string itsRuntimeCore = result[1]; 48 | string itsRuntimeModule = result[2]; 49 | await Assert.That(result.Length).IsEqualTo(4); 50 | await Verify($""" 51 | --------- 52 | TSRuntime 53 | --------- 54 | 55 | {tsRuntime.XVersionNumber()} 56 | 57 | ---------- 58 | ITSRuntime 59 | ---------- 60 | 61 | {itsRuntimeCore.XVersionNumber()} 62 | 63 | ------ 64 | Module 65 | ------ 66 | 67 | {itsRuntimeModule} 68 | """); 69 | } 70 | 71 | [Test] 72 | public async ValueTask JSGenericsAndTypeMap() { 73 | const string jsonConfig = """{}"""; 74 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/GenericModule.d.ts", "export function genericKeyofConstraint(): number;\n"); 75 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 76 | await Assert.That(diagnostics).IsEmpty(); 77 | 78 | string tsRuntime = result[0]; 79 | string itsRuntimeCore = result[1]; 80 | string itsRuntimeModule = result[2]; 81 | await Assert.That(result.Length).IsEqualTo(4); 82 | await Verify($""" 83 | --------- 84 | TSRuntime 85 | --------- 86 | 87 | {tsRuntime.XVersionNumber()} 88 | 89 | ---------- 90 | ITSRuntime 91 | ---------- 92 | 93 | {itsRuntimeCore.XVersionNumber()} 94 | 95 | ------ 96 | Module 97 | ------ 98 | 99 | {itsRuntimeModule} 100 | """); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Readme_md/InputPath.md: -------------------------------------------------------------------------------- 1 | # Config - Webroot Path and Input Path 2 | 3 | ## Webroot Path 4 | 5 | The relative path to the web root from where the path gets resolved. 6 | Normally the relative path is your project root directory, then .js-files attached at razor components get resolved correctly 7 | and .js files in the wwwroot-folder have also the right path, because the starting 'wwwroot' folder is ignored. 8 | When you place your tsruntime.json file in the project root directory, then you can just let this value alone. 9 | But if you want to move this file somewhere else, e.g. in a 'config' folder, you can set *web root path* to "..", 10 | so the root path still starts at your project root directory. 11 | 12 | 13 |

14 | ## Config - Input Path 15 | 16 | You can set "input path" just to a string 17 | 18 | ```json 19 | { 20 | "input path": "/jsFolder" 21 | } 22 | ``` 23 | 24 | This will include all files inside "/jsFolder". 25 | 26 | But if you want you can also be more accurate. The previous example is just a shorthand for: 27 | 28 | ```json 29 | { 30 | "input path": [ 31 | { 32 | "include": "/jsFolder", 33 | "excludes": [], 34 | "module files": true, 35 | "module path": null 36 | } 37 | ] 38 | } 39 | ``` 40 | 41 | So, you can have a list of include paths. Each path can be a folder or a file. 42 | Each include path can have a list of exclude paths, each can be a folder or a file. 43 | An exclude path must start with the same as include path in order to match. 44 | 45 | **Example**: 46 | ```json 47 | { 48 | "input path": [ 49 | { 50 | "include": "/jsFolder", 51 | "excludes": [ 52 | "/jsFolder/private", 53 | "/jsFolder/wwwroot/service-worker.js" 54 | ] 55 | }, 56 | "/otherInputFolder" 57 | ] 58 | } 59 | ``` 60 | 61 | The preceding configuration has two include paths "/jsFolder" and "/otherInputFolder" 62 | and inside "/jsFolder" the folder "private" and inside "wwwroot" the file "service-worker.js" will not be included. 63 | 64 | 65 | ### Module Files 66 | 67 | A flag that can be set to false to read in a folder/file where global scripts are located 68 | (files that are placed in html with the <script> tag). 69 | 70 | **Example**: 71 | ```html 72 | 73 | 74 | 75 | ``` 76 | 77 | ```json 78 | { 79 | "input path": { 80 | "include": "/wwwroot/js", 81 | "module files": false 82 | } 83 | } 84 | ``` 85 | 86 | In the preceding example all files located in the *wwwroot/js* folder are included as global scripts. 87 | 88 | Note: 89 | To recognize a function in a global script as callable function, the line must start with "function", other types of declarations are ignored. 90 | If you have multiple *input path* and they intersect, the first one in the list has priority. 91 | So put the specific rules at the top and the general rules at the bottom. Or make sure to exclude sections that intersect. 92 | 93 | 94 | ### Module Path 95 | 96 | If your include path is a file, the module path will be the same as your include path. 97 | If that path does not fit, you can set it explicit with [module path]. 98 | 99 | **Example**: 100 | ```json 101 | { 102 | "input path": { 103 | "include": "/scripts/declarations/shared.js", 104 | "module path": "/scripts/shared.js" 105 | } 106 | } 107 | ``` 108 | 109 | The preceding configuration only reads in one module: "shared.js". 110 | If [module path] would be not set, the module path would be "/scripts/declarations/shared.js". 111 | Because the actual script is served on the URL "/scripts/shared.js", it has to be set explicitly. 112 | If a value is provided for [module path], it should end with ".js", regardless of the include type (*.js*, *.ts*, *.d.ts*). 113 | 114 | Setting explicit module path for folders is not supported and will result in errors (duplicate hintNames). 115 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GenerateSourceTextExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Text; 4 | using System.Collections.Immutable; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace TSRuntime.Tests; 10 | 11 | public static partial class GenerateSourceTextExtension { 12 | public const string CONFIG_FOLDER_PATH = @"C:\SomeAbsolutePath"; 13 | 14 | 15 | [GeneratedRegex(@"(\d+\.\d+\.\d+)")] 16 | private static partial Regex VersionNumberRegex(); 17 | 18 | public static string XVersionNumber(this string input) => VersionNumberRegex().Replace(input, "X.X.X"); 19 | 20 | 21 | /// 22 | /// Takes additional files as input and outputs the generated source code based on the given input. 23 | /// The generated source code contains post-initialization-output code as well as source output code. 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | public static string[] GenerateSourceText(this string config, (string path, string content)[] input, out Compilation outputCompilation, out ImmutableArray diagnostics) 30 | => [.. config.GenerateSourceResult(input, out outputCompilation, out diagnostics).Select((GeneratedSourceResult result) => result.SourceText.ToString())]; 31 | 32 | 33 | /// 34 | /// Takes additional files as input and outputs the generated source code based on the given input. 35 | /// The generated source code contains post-initialization-output code as well as source output code. 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | public static ImmutableArray GenerateSourceResult(this string config, (string path, string content)[] input, out Compilation outputCompilation, out ImmutableArray diagnostics) { 42 | TSRuntimeGenerator generator = new(); 43 | AdditionalText configFile = new InMemoryAdditionalText($"{CONFIG_FOLDER_PATH}/tsruntime.json", config); 44 | IEnumerable modules = input.Select<(string, string), AdditionalText>(((string path, string content) file) => new InMemoryAdditionalText(file.path, file.content)); 45 | 46 | GeneratorDriver driver = CSharpGeneratorDriver.Create(generator); 47 | driver = driver.AddAdditionalTexts([configFile, .. modules]); 48 | driver = driver.RunGeneratorsAndUpdateCompilation(CreateCompilation(string.Empty), out outputCompilation, out diagnostics); 49 | 50 | GeneratorDriverRunResult runResult = driver.GetRunResult(); 51 | GeneratorRunResult generatorResult = runResult.Results[0]; 52 | return generatorResult.GeneratedSources; 53 | 54 | 55 | static CSharpCompilation CreateCompilation(string source) { 56 | SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); 57 | PortableExecutableReference metadataReference = MetadataReference.CreateFromFile(typeof(Binder).Assembly.Location); 58 | CSharpCompilationOptions compilationOptions = new(OutputKind.DynamicallyLinkedLibrary); 59 | 60 | return CSharpCompilation.Create("compilation", [syntaxTree], [metadataReference], compilationOptions); 61 | } 62 | } 63 | 64 | 65 | private sealed class InMemoryAdditionalText(string path, string text) : AdditionalText { 66 | private sealed class InMemorySourceText(string text) : SourceText { 67 | public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) => text.CopyTo(sourceIndex, destination, destinationIndex, count); 68 | 69 | public override Encoding? Encoding => Encoding.Default; 70 | public override int Length => text.Length; 71 | 72 | public override char this[int position] => text[position]; 73 | } 74 | 75 | public override string Path { get; } = path; 76 | 77 | public override SourceText? GetText(CancellationToken cancellationToken = default) => new InMemorySourceText(text); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/NamePattern/ModuleNamePattern.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Text; 3 | 4 | namespace TSRuntime.Configs.NamePattern; 5 | 6 | /// 7 | /// Naming with 1 variable: #module#. 8 | /// 9 | public readonly struct ModuleNamePattern : IEquatable { 10 | private readonly List outputList = new(3); // default "I#module#Module" are 3 entries 11 | /// 12 | /// The name pattern for creating the name. 13 | /// 14 | /// placeholder:
15 | /// #module# 16 | ///
17 | ///
18 | public string NamePattern { get; } 19 | /// 20 | /// Upper/Lower case transform for the #module# placeholder. 21 | /// 22 | public NameTransform ModuleTransform { get; } 23 | 24 | 25 | /// 26 | /// Parses the given namePattern to construct an outputList. 27 | /// 28 | /// 29 | /// The name pattern for creating the method name. 30 | /// placeholder:
#module#
31 | /// 32 | /// Upper/Lower case transform for the #module# placeholder. 33 | /// 34 | public ModuleNamePattern(string namePattern, NameTransform moduleTransform, List errorList) { 35 | NamePattern = namePattern; 36 | ModuleTransform = moduleTransform; 37 | 38 | 39 | ReadOnlySpan str = namePattern.AsSpan(); 40 | 41 | while (str.Length > 0) { 42 | // first '#' 43 | int index = str.IndexOf('#'); 44 | 45 | // has no "#" 46 | if (index == -1) { 47 | if (str.Length > 0) 48 | outputList.Add(str.ToString()); 49 | return; 50 | } 51 | 52 | // read in [..#] 53 | if (index > 0) { 54 | outputList.Add(str[..index].ToString()); 55 | str = str[index..]; 56 | } 57 | 58 | 59 | // second '#' 60 | index = str[1..].IndexOf('#') + 1; 61 | 62 | // has no second '#' 63 | if (index == 0) { 64 | errorList.AddConfigNamePatternMissingEndTagError(); 65 | return; 66 | } 67 | 68 | // read in [#..#] 69 | int length = index + 1; 70 | if (str[..length] is ['#', 'm', 'o', 'd', 'u', 'l', 'e', '#']) 71 | outputList.Add(Output.Module); 72 | else 73 | errorList.AddConfigNamePatternInvalidVariableError(str[1..index].ToString(), ["module"]); 74 | 75 | 76 | str = str[length..]; 77 | } 78 | } 79 | 80 | /// 81 | /// Appends the name based on the values of this object and the given parameters. 82 | /// 83 | /// 84 | /// Name of the module. 85 | /// 86 | public readonly void AppendNaming(StringBuilder builder, string module) { 87 | string moduleName = ModuleTransform.Transform(module); 88 | 89 | foreach (OutputBlock block in outputList) 90 | builder.Append(block.Output switch { 91 | Output.Module => moduleName, 92 | Output.String => block.Content, 93 | _ => throw new Exception("not reachable") 94 | }); 95 | } 96 | 97 | 98 | #region IEquatable 99 | 100 | public static bool operator ==(ModuleNamePattern left, ModuleNamePattern right) => left.Equals(right); 101 | 102 | public static bool operator !=(ModuleNamePattern left, ModuleNamePattern right) => !left.Equals(right); 103 | 104 | public override bool Equals(object obj) 105 | => obj switch { 106 | ModuleNamePattern other => Equals(other), 107 | _ => false 108 | }; 109 | 110 | public bool Equals(ModuleNamePattern other) { 111 | if (NamePattern != other.NamePattern) 112 | return false; 113 | 114 | if (ModuleTransform != other.ModuleTransform) 115 | return false; 116 | 117 | return true; 118 | } 119 | 120 | public override int GetHashCode() { 121 | int hashCode = NamePattern.GetHashCode(); 122 | 123 | hashCode = Combine(hashCode, ModuleTransform.GetHashCode()); 124 | 125 | return hashCode; 126 | 127 | static int Combine(int h1, int h2) { 128 | uint r = (uint)h1 << 5 | (uint)h1 >> 27; 129 | return (int)r + h1 ^ h2; 130 | } 131 | } 132 | 133 | #endregion 134 | } 135 | -------------------------------------------------------------------------------- /Readme_md/TypeMap.md: -------------------------------------------------------------------------------- 1 | # Config - Type Map 2 | 3 | This map defines all types that are convertible between the languages. Types with the same name does not need to be listed. 4 | Keep in mind that the JSRuntime conversion logic must support the mapping, otherwise you will end up with the wrong type. 5 | 6 |

7 | To define a convertible pair, set the TS-type as key and the C#-type as value: 8 | 9 | ```json 10 | "type map": { 11 | "number": "double" 12 | } 13 | ``` 14 | 15 | Generic types are not detected automatically and must be specified explicitly. 16 | The above example is actually a shorthand: 17 | 18 | ```json 19 | "type map": { 20 | "number": { 21 | "type": "double", 22 | "generic types": [] 23 | } 24 | } 25 | ``` 26 | 27 | So, if your type depends on one or more generic types, specify it in "generic types": 28 | 29 | ```json 30 | "type map": { 31 | "number": { 32 | "type": "TNumber", 33 | "generic types": "TNumber" 34 | } 35 | } 36 | ``` 37 | 38 | This is once again a shorthand for: 39 | 40 | ```json 41 | "type map": { 42 | "number": { 43 | "type": "TNumber", 44 | "generic types": [ 45 | { 46 | "name": "TNumber", 47 | "constraint": null 48 | } 49 | ] 50 | } 51 | } 52 | ``` 53 | 54 | At last you want a type constraint on INumber<TSelf>, 55 | so you get a working mapping from *number* to *INumber<TSelf>*: 56 | 57 | ```json 58 | "type map": { 59 | "number": { 60 | "type": "TNumber", 61 | "generic types": { 62 | "name": "TNumber", 63 | "constraint": "INumber" 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | If you want to add multiple constraints on a type, just separate them with ','. 70 | Here a final complete example: 71 | 72 | ```json 73 | "type map": { 74 | "JSType": { 75 | "type": "CSharpType", 76 | "generic types": [ 77 | { 78 | "name": "TType1", 79 | "constraint": "constraint1, constraint2, ..." 80 | }, 81 | { 82 | "name": "TType2", 83 | "constraint": "IDisposable, new()" 84 | } 85 | ] 86 | } 87 | } 88 | ``` 89 | 90 | **Note**: generic-type naming conflicts are not detected nor handled, so make sure your generic types are named uniquely. 91 | 92 | 93 |

94 | ## Nullable/Optional 95 | 96 | Variants of nullable or optional are not concidered as different types. 97 | If a TS-variable is nullable, it is also nullable in C#. 98 | If a TS-variable is optional/undefined, it is also optional in C# by creating overload methods, but only if the last parameters are optional/undefined. 99 | If an array item is undefined, it is treated like nullable. 100 | 101 | Here are some examples: 102 | 103 | | TypeScript | C# | 104 | | -------------------------------------------------------- | -------------------------------- | 105 | | do(myParameter: string) | Do(string myParameter) | 106 | | do(myParameter: string \| null) | Do(string? myParameter) | 107 | | do(myParameter?: string) | Do(), Do(string myParameter) | 108 | | do(myParameter: string \| undefined) | Do(), Do(string myParameter) | 109 | | do(myParameter?: string \| undefined) | Do(), Do(string myParameter) | 110 | | do(myParameter?: string \| null) | Do(), Do(string? myParameter) | 111 | | do(myParameter: string \| null \| undefined) | Do(), Do(string? myParameter) | 112 | | do(myParameter: (string \| null)[]) | Do(string?[] myParameter) | 113 | | do(myParameter: (string \| undefined)[]) | Do(string?[] myParameter) | 114 | | do(myParameter: (string \| null \| undefined)[]) | Do(string?[] myParameter) | 115 | | do(myParameter: (string \| null)[] \| null) | Do(), Do(string?[]? myParameter) | 116 | | do(myParameter: (string \| null)[] \| undefined) | Do(), Do(string?[] myParameter) | 117 | | do(myParameter: (string \| null)[] \| null \| undefined) | Do(), Do(string?[]? myParameter) | 118 | 119 | **Note**: default value parameters (e.g. do(myParameter = 5)) are automatically mapped to optional parameters in .d.ts-files, so they will work as expected. 120 | 121 | 122 |

123 | ## Generics 124 | 125 | If you are using generic functions and want to map generic variables, the generic variable name must match. 126 | For example, *Map<T>* is treated as another type than *Map<Type>*. 127 | Mapping of generic constraints is not supported. 128 | You can use constraints in your JS/TS functions, but they are just ignored, so on the C# side it will be an unconstraint type paramter. 129 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Parsing/TSFile/TSFile.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Parsing; 2 | 3 | /// 4 | /// Represents a js/ts/d.ts-file. 5 | /// 6 | public abstract class TSFile : IEquatable { 7 | /// 8 | /// The raw given filePath to the module. 9 | /// 10 | public string FilePath { get; protected init; } = string.Empty; 11 | 12 | /// 13 | /// The but it is relative, starts with "/" and ends with ".js", also ignoring starting "/wwwroot". 14 | /// 15 | public string URLPath { get; protected init; } = string.Empty; 16 | 17 | /// 18 | /// fileName without ending ".d.ts/.ts/.js" or ".razor" and not allowed variable-characters are replaced with '_'. 19 | /// 20 | public string Name { get; protected init; } = string.Empty; 21 | 22 | /// 23 | /// List of js-functions of the module/script. 24 | /// 25 | public IReadOnlyList FunctionList { get; protected init; } = []; 26 | 27 | 28 | /// 29 | /// removes extension ".js"/".ts"/".d.ts", skips leading "wwwroot" and makes sure it starts with '/'. 30 | /// 31 | /// 32 | /// 33 | /// 34 | protected string CreateURLPath(ref ReadOnlySpan path) { 35 | path = path switch { 36 | [.., '.', 'd', '.', 't', 's'] => path[..^5], // skip ".d.ts" 37 | [.., '.', 'j', 's'] or [.., '.', 't', 's'] => path[..^3], // skip ".js"/".ts" 38 | _ => throw new Exception("Unreachable: must be already filtered in InputPath.IsIncluded") 39 | }; 40 | 41 | if (path is ['w', 'w', 'w', 'r', 'o', 'o', 't', '/', ..]) 42 | path = path[8..]; // skip "wwwroot/" 43 | 44 | if (path is ['/', ..]) 45 | return $"{path.ToString()}.js"; 46 | else 47 | return $"/{path.ToString()}.js"; 48 | } 49 | 50 | /// 51 | /// Retrieves the file name of the path (without ".razor") and replaces unsafe characters with "_". 52 | /// 53 | /// 54 | /// 55 | protected string CreateModuleName(ReadOnlySpan path) { 56 | // FileName 57 | int lastSlash = path.LastIndexOf('/'); 58 | ReadOnlySpan rawModuleName = (lastSlash != -1) switch { 59 | true => path[(lastSlash + 1)..], 60 | false => path 61 | }; 62 | 63 | if (rawModuleName is [.., '.', 'r', 'a', 'z', 'o', 'r']) 64 | rawModuleName = rawModuleName[..^6]; // skip ".razor" 65 | 66 | if (rawModuleName.Length > 0) { 67 | Span saveModuleName = stackalloc char[rawModuleName.Length + 1]; 68 | int startIndex; 69 | if (char.IsDigit(rawModuleName[0])) { 70 | saveModuleName[0] = '_'; 71 | startIndex = 1; 72 | } 73 | else { 74 | saveModuleName = saveModuleName[1..]; 75 | startIndex = 0; 76 | } 77 | for (int i = startIndex; i < saveModuleName.Length; i++) 78 | saveModuleName[i] = char.IsLetterOrDigit(rawModuleName[i]) switch { 79 | true => rawModuleName[i], 80 | false => '_' 81 | }; 82 | 83 | return saveModuleName.ToString(); 84 | } 85 | else 86 | return string.Empty; 87 | } 88 | 89 | 90 | #region IEquatable 91 | 92 | public static bool operator ==(TSFile left, TSFile right) => left.Equals(right); 93 | 94 | public static bool operator !=(TSFile left, TSFile right) => !left.Equals(right); 95 | 96 | public override bool Equals(object obj) 97 | => obj switch { 98 | TSFile other => Equals(other), 99 | _ => false 100 | }; 101 | 102 | public bool Equals(TSFile other) { 103 | if (FilePath != other.FilePath) 104 | return false; 105 | 106 | if (URLPath != other.URLPath) 107 | return false; 108 | 109 | if (Name != other.Name) 110 | return false; 111 | 112 | if (!FunctionList.SequenceEqual(other.FunctionList)) 113 | return false; 114 | 115 | return true; 116 | } 117 | 118 | public override int GetHashCode() { 119 | int hash = FilePath.GetHashCode(); 120 | hash = Combine(hash, URLPath.GetHashCode()); 121 | hash = Combine(hash, Name.GetHashCode()); 122 | 123 | foreach (TSFunction tsFunction in FunctionList) 124 | hash = Combine(hash, tsFunction.GetHashCode()); 125 | 126 | return hash; 127 | 128 | 129 | static int Combine(int h1, int h2) { 130 | uint r = (uint)h1 << 5 | (uint)h1 >> 27; 131 | return (int)r + h1 ^ h2; 132 | } 133 | } 134 | 135 | #endregion 136 | } 137 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Blazor.TSRuntime.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 14 6 | enable 7 | enable 8 | 9 | Blazor.TSRuntime 10 | TSRuntime is an improved JSRuntime with automatic JS-module loading and caching, compile time errors instead of runtime errors and nice IntelliSense guidance. 11 | BlackWhiteYoshi 12 | C#;.Net;Blazor;JS;JavaScript;TS;TypeScript;JSRuntime;Interop 13 | 14 | PACKAGE.md 15 | https://github.com/BlackWhiteYoshi/Blazor.TSRuntime 16 | 17 | git 18 | https://github.com/BlackWhiteYoshi/Blazor.TSRuntime.git 19 | true 20 | 21 | https://raw.githubusercontent.com/BlackWhiteYoshi/Blazor.TSRuntime/main/ICON.png 22 | ICON.png 23 | 24 | LICENSE 25 | false 26 | 27 | true 28 | false 29 | true 30 | true 31 | true 32 | 33 | 1.0.2 34 | 35 | 36 | 37 | true 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | $(GetTargetPathDependsOn);GetDependencyTargetPaths 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/Types/InputPath.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Configs; 2 | 3 | public readonly struct InputPath(string include, string[] excludes) : IEquatable { 4 | /// 5 | /// Path to a folder. Every .d.ts-file in that folder will be included. 6 | /// It can also be a path to a file. If this is a file-path, must also be set. 7 | /// No trailing slash. 8 | /// 9 | public string Include { get; init; } = include; 10 | 11 | /// 12 | /// Excludes specific folders or files from . 13 | /// 14 | /// Every path must start with the path given in , otherwise that path won't match.
15 | /// No trailing slash allowed, otherwise that path won't match. 16 | ///
17 | ///
18 | public string[] Excludes { get; init; } = excludes; 19 | 20 | /// 21 | /// Indicates if the files in located at this path are modules or scripts. 22 | /// 23 | public bool ModuleFiles { get; init; } = true; 24 | 25 | /// 26 | /// 27 | /// Relative Path/URL to load the module.
28 | /// e.g. "Pages/Footer/Contacts.razor.js" 29 | ///
30 | /// If is a folder path, this does nothing. 31 | ///
32 | public string? ModulePath { get; init; } = null; 33 | 34 | 35 | /// 36 | /// Sets to given string and to an empty array. 37 | /// 38 | /// 39 | public InputPath(string include) : this(include, []) { } 40 | 41 | /// 42 | /// Sets all parameters of this data structure. 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | public InputPath(string include, string[] excludes, bool moduleFiles, string? modulePath) : this(include, excludes) { 49 | ModuleFiles = moduleFiles; 50 | ModulePath = modulePath; 51 | } 52 | 53 | 54 | public void Deconstruct(out string include, out string[] excludes, out bool moduleFiles, out string? modulePath) { 55 | include = Include; 56 | excludes = Excludes; 57 | moduleFiles = ModuleFiles; 58 | modulePath = ModulePath; 59 | } 60 | 61 | 62 | /// 63 | /// Checks if the filePath is not in the given exclude list. 64 | /// filePath and excludes must start with the same characters.
65 | /// exclude paths must not end with trailing slash.
66 | ///
67 | /// 68 | /// 69 | public bool IsIncluded(string filePath) { 70 | if (!filePath.StartsWith(Include) || filePath is not ([.., '.', 'j', 's'] or [.., '.', 't', 's'])) // ".d.ts" ends with ".ts" 71 | return false; 72 | 73 | foreach (string exclude in Excludes) { 74 | if (exclude.Length == 0) 75 | return false; 76 | 77 | if (filePath.StartsWith(exclude)) { 78 | // exclude is file 79 | if (filePath.Length == exclude.Length) 80 | return false; 81 | 82 | // exclude is folder 83 | if (filePath[exclude.Length] == '/') 84 | return false; 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | 92 | #region IEquatable 93 | 94 | public static bool operator ==(InputPath left, InputPath right) => left.Equals(right); 95 | 96 | public static bool operator !=(InputPath left, InputPath right) => !left.Equals(right); 97 | 98 | public override bool Equals(object obj) 99 | => obj switch { 100 | InputPath other => Equals(other), 101 | _ => false 102 | }; 103 | 104 | public bool Equals(InputPath other) { 105 | if (Include != other.Include) 106 | return false; 107 | 108 | if (!Excludes.SequenceEqual(other.Excludes)) 109 | return false; 110 | 111 | if (!ModuleFiles != other.ModuleFiles) 112 | return false; 113 | 114 | if (ModulePath != other.ModulePath) 115 | return false; 116 | 117 | return true; 118 | } 119 | 120 | public override int GetHashCode() { 121 | int hash = Include.GetHashCode(); 122 | 123 | foreach (string exclude in Excludes) 124 | hash = Combine(hash, exclude.GetHashCode()); 125 | 126 | hash = Combine(hash, ModuleFiles.GetHashCode()); 127 | 128 | hash = Combine(hash, ModulePath?.GetHashCode() ?? 0); 129 | 130 | return hash; 131 | 132 | 133 | static int Combine(int h1, int h2) { 134 | uint r = (uint)h1 << 5 | (uint)h1 >> 27; 135 | return (int)r + h1 ^ h2; 136 | } 137 | } 138 | 139 | #endregion 140 | } 141 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/ScriptTests/ScriptTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Collections.Immutable; 3 | 4 | namespace TSRuntime.Tests; 5 | 6 | public sealed class ScriptTests { 7 | private const string SCRIPT_PATH = $"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/site.ts"; 8 | 9 | [Test] 10 | public async ValueTask ParameterlessFunction() { 11 | const string jsonConfig = """ 12 | { 13 | "input path": { 14 | "include": "/", 15 | "module files": false 16 | }, 17 | "invoke function": { 18 | "sync enabled": true, 19 | "trysync enabled": true, 20 | "async enabled": true, 21 | "name pattern": { 22 | "pattern": "#function##action#", 23 | "module transform": "first upper case", 24 | "function transform": "first upper case", 25 | "action transform": "none" 26 | } 27 | } 28 | } 29 | """; 30 | const string scriptFunction = "function Test() {}\n"; 31 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, scriptFunction)], out _, out ImmutableArray diagnostics); 32 | await Assert.That(diagnostics).IsEmpty(); 33 | 34 | string itsRuntimeScript = result[2]; 35 | await Verify(itsRuntimeScript); 36 | } 37 | 38 | [Test] 39 | public async ValueTask ParameterAndReturnTypeFunction() { 40 | const string jsonConfig = """ 41 | { 42 | "input path": { 43 | "include": "/", 44 | "module files": false 45 | }, 46 | "invoke function": { 47 | "sync enabled": true, 48 | "trysync enabled": true, 49 | "async enabled": true, 50 | "name pattern": { 51 | "pattern": "#function##action#", 52 | "module transform": "first upper case", 53 | "function transform": "first upper case", 54 | "action transform": "none" 55 | } 56 | } 57 | } 58 | """; 59 | const string scriptFunction = "function Test(str: string, a: boolean): number {}\n"; 60 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, scriptFunction)], out _, out ImmutableArray diagnostics); 61 | await Assert.That(diagnostics).IsEmpty(); 62 | 63 | string itsRuntimeScript = result[2]; 64 | await Verify(itsRuntimeScript); 65 | } 66 | 67 | [Test] 68 | public async ValueTask PromiseFunction() { 69 | const string jsonConfig = """ 70 | { 71 | "input path": { 72 | "include": "/", 73 | "module files": false 74 | }, 75 | "invoke function": { 76 | "sync enabled": true, 77 | "trysync enabled": true, 78 | "async enabled": true, 79 | "name pattern": { 80 | "pattern": "#function##action#", 81 | "module transform": "first upper case", 82 | "function transform": "first upper case", 83 | "action transform": "none" 84 | } 85 | } 86 | } 87 | """; 88 | const string scriptFunction = "function Test(): Promise {}\n"; 89 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, scriptFunction)], out _, out ImmutableArray diagnostics); 90 | await Assert.That(diagnostics).IsEmpty(); 91 | 92 | string itsRuntimeScript = result[2]; 93 | await Verify(itsRuntimeScript); 94 | } 95 | 96 | [Test] 97 | public async ValueTask PromiseReturnFunction() { 98 | const string jsonConfig = """ 99 | { 100 | "input path": { 101 | "include": "/", 102 | "module files": false 103 | }, 104 | "invoke function": { 105 | "sync enabled": true, 106 | "trysync enabled": true, 107 | "async enabled": true, 108 | "name pattern": { 109 | "pattern": "#function##action#", 110 | "module transform": "first upper case", 111 | "function transform": "first upper case", 112 | "action transform": "none" 113 | } 114 | } 115 | } 116 | """; 117 | const string scriptFunction = "function Test(): Promise {}\n"; 118 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, scriptFunction)], out _, out ImmutableArray diagnostics); 119 | await Assert.That(diagnostics).IsEmpty(); 120 | 121 | string itsRuntimeScript = result[2]; 122 | await Verify(itsRuntimeScript); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Configs/NamePattern/FunctionNamePattern.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Text; 3 | 4 | namespace TSRuntime.Configs.NamePattern; 5 | 6 | /// 7 | /// Naming of the generated methods that invoke js-functions. 8 | /// It supports the variables #function#, #module# and #action#. 9 | /// 10 | public readonly struct FunctionNamePattern : IEquatable { 11 | private readonly List outputList = new(1); // default "#function#" is 1 entry 12 | /// 13 | /// The name pattern for creating the method name. 14 | /// placeholder:
15 | /// #function#
16 | /// #module#
17 | /// #action#
18 | ///
19 | public string NamePattern { get; } 20 | /// 21 | /// Upper/Lower case transform for the #module# placeholder. 22 | /// 23 | public NameTransform ModuleTransform { get; } 24 | /// 25 | /// Upper/Lower case transform for the #function# placeholder. 26 | /// 27 | public NameTransform FunctionTransform { get; } 28 | /// 29 | /// Upper/Lower case transform for the #action# placeholder. 30 | /// 31 | public NameTransform ActionTransform { get; } 32 | 33 | 34 | /// 35 | /// Parses the given namePattern to construct an outputList. 36 | /// 37 | /// 38 | /// The name pattern for creating the method name. 39 | /// placeholder:
40 | /// #function#
41 | /// #module#
42 | /// #action#
43 | /// 44 | /// Upper/Lower case transform for the #module# placeholder. 45 | /// Upper/Lower case transform for the #function# placeholder. 46 | /// Upper/Lower case transform for the #action# placeholder. 47 | /// Is only used when input is invalud. 48 | public FunctionNamePattern(string namePattern, NameTransform moduleTransform, NameTransform functionTransform, NameTransform actionTransform, List errorList) { 49 | NamePattern = namePattern; 50 | ModuleTransform = moduleTransform; 51 | FunctionTransform = functionTransform; 52 | ActionTransform = actionTransform; 53 | 54 | 55 | ReadOnlySpan str = namePattern.AsSpan(); 56 | 57 | while (str.Length > 0) { 58 | // first '#' 59 | int index = str.IndexOf('#'); 60 | 61 | // has no '#' 62 | if (index == -1) { 63 | if (str.Length > 0) 64 | outputList.Add(str.ToString()); 65 | return; 66 | } 67 | 68 | // read in [..#] 69 | if (index > 0) { 70 | outputList.Add(str[..index].ToString()); 71 | str = str[index..]; 72 | } 73 | 74 | 75 | // second '#' 76 | index = str[1..].IndexOf('#') + 1; 77 | 78 | // has no second '#' 79 | if (index == 0) { 80 | errorList.AddConfigNamePatternMissingEndTagError(); 81 | return; 82 | } 83 | 84 | // read in [#..#] 85 | int length = index + 1; 86 | switch (str[..length]) { 87 | case ['#', 'm', 'o', 'd', 'u', 'l', 'e', '#']: 88 | outputList.Add(Output.Module); 89 | break; 90 | case ['#', 'a', 'c', 't', 'i', 'o', 'n', '#', ..]: 91 | outputList.Add(Output.Action); 92 | break; 93 | case ['#', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', '#', ..]: 94 | outputList.Add(Output.Function); 95 | break; 96 | default: 97 | errorList.AddConfigNamePatternInvalidVariableError(str[1..index].ToString(), ["module", "function", "action"]); 98 | break; 99 | } 100 | 101 | 102 | str = str[length..]; 103 | } 104 | } 105 | 106 | /// 107 | /// Appends the name of the method based on the values of this object and the given parameters. 108 | /// 109 | /// 110 | /// Name of the module. 111 | /// Name of the function. 112 | /// Name of the action. 113 | /// 114 | public readonly void AppendNaming(StringBuilder builder, string module, string function, string action) { 115 | string moduleName = ModuleTransform.Transform(module); 116 | string functionName = FunctionTransform.Transform(function); 117 | string actionName = ActionTransform.Transform(action); 118 | 119 | foreach (OutputBlock block in outputList) 120 | builder.Append(block.Output switch { 121 | Output.Module => moduleName, 122 | Output.Function => functionName, 123 | Output.Action => actionName, 124 | Output.String => block.Content, 125 | _ => throw new Exception("not reachable") 126 | }); 127 | } 128 | 129 | 130 | #region IEquatable 131 | 132 | public static bool operator ==(FunctionNamePattern left, FunctionNamePattern right) => left.Equals(right); 133 | 134 | public static bool operator !=(FunctionNamePattern left, FunctionNamePattern right) => !left.Equals(right); 135 | 136 | public override bool Equals(object obj) 137 | => obj switch { 138 | FunctionNamePattern other => Equals(other), 139 | _ => false 140 | }; 141 | 142 | public bool Equals(FunctionNamePattern other) { 143 | if (NamePattern != other.NamePattern) 144 | return false; 145 | 146 | if (ModuleTransform != other.ModuleTransform) 147 | return false; 148 | if (FunctionTransform != other.FunctionTransform) 149 | return false; 150 | if (ActionTransform != other.ActionTransform) 151 | return false; 152 | 153 | return true; 154 | } 155 | 156 | public override int GetHashCode() { 157 | int hashCode = NamePattern.GetHashCode(); 158 | 159 | hashCode = Combine(hashCode, ModuleTransform.GetHashCode()); 160 | hashCode = Combine(hashCode, FunctionTransform.GetHashCode()); 161 | hashCode = Combine(hashCode, ActionTransform.GetHashCode()); 162 | 163 | return hashCode; 164 | 165 | static int Combine(int h1, int h2) { 166 | uint r = (uint)h1 << 5 | (uint)h1 >> 27; 167 | return (int)r + h1 ^ h2; 168 | } 169 | } 170 | 171 | #endregion 172 | } 173 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/SummaryTests/GeneratorSummaryTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Collections.Immutable; 3 | 4 | namespace TSRuntime.Tests; 5 | 6 | public sealed class GeneratorSummaryTests { 7 | [Test] 8 | public async ValueTask SummaryOnly() { 9 | const string jsonConfig = """{}"""; 10 | const string moduleContent = """ 11 | /** 12 | * The example Summary 13 | */ 14 | export function test(): void; 15 | 16 | """; 17 | 18 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.d.ts", moduleContent); 19 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 20 | await Assert.That(diagnostics).IsEmpty(); 21 | 22 | await Assert.That(result.Length).IsEqualTo(4); 23 | string itsRuntimeModule = result[2]; 24 | await Verify(itsRuntimeModule); 25 | } 26 | 27 | [Test] 28 | public async ValueTask RemarksOnly() { 29 | const string jsonConfig = """{}"""; 30 | const string moduleContent = """ 31 | /** 32 | * @remarks The example remark 33 | */ 34 | export function test(): void; 35 | 36 | """; 37 | 38 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.d.ts", moduleContent); 39 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 40 | await Assert.That(diagnostics).IsEmpty(); 41 | 42 | await Assert.That(result.Length).IsEqualTo(4); 43 | string itsRuntimeModule = result[2]; 44 | await Verify(itsRuntimeModule); 45 | } 46 | 47 | [Test] 48 | public async ValueTask ParamOnly() { 49 | const string jsonConfig = """{}"""; 50 | const string moduleContent = """ 51 | /** 52 | * @param a - a is not B 53 | */ 54 | export function test(a: number): void; 55 | 56 | """; 57 | 58 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.d.ts", moduleContent); 59 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 60 | await Assert.That(diagnostics).IsEmpty(); 61 | 62 | await Assert.That(result.Length).IsEqualTo(4); 63 | string itsRuntimeModule = result[2]; 64 | await Verify(itsRuntimeModule); 65 | } 66 | 67 | [Test] 68 | public async ValueTask ParamOnly_JSDoc() { 69 | const string jsonConfig = """{}"""; 70 | const string moduleContent = """ 71 | /** 72 | * @param {number} a - a is not B 73 | */ 74 | export function test(a) { } 75 | 76 | """; 77 | 78 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.js", moduleContent); 79 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 80 | await Assert.That(diagnostics).IsEmpty(); 81 | 82 | await Assert.That(result.Length).IsEqualTo(4); 83 | string itsRuntimeModule = result[2]; 84 | await Verify(itsRuntimeModule); 85 | } 86 | 87 | [Test] 88 | public async ValueTask ReturnsOnly() { 89 | const string jsonConfig = """{}"""; 90 | const string moduleContent = """ 91 | /** 92 | * @returns a string 93 | */ 94 | export function test(): string; 95 | 96 | """; 97 | 98 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.d.ts", moduleContent); 99 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 100 | await Assert.That(diagnostics).IsEmpty(); 101 | 102 | await Assert.That(result.Length).IsEqualTo(4); 103 | string itsRuntimeModule = result[2]; 104 | await Verify(itsRuntimeModule); 105 | } 106 | 107 | [Test] 108 | public async ValueTask ReturnsOnly_JSDoc() { 109 | const string jsonConfig = """{}"""; 110 | const string moduleContent = """ 111 | /** 112 | * @returns {string} a string 113 | */ 114 | export function test() { } 115 | 116 | """; 117 | 118 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.js", moduleContent); 119 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 120 | await Assert.That(diagnostics).IsEmpty(); 121 | 122 | await Assert.That(result.Length).IsEqualTo(4); 123 | string itsRuntimeModule = result[2]; 124 | await Verify(itsRuntimeModule); 125 | } 126 | 127 | 128 | [Test] 129 | public async ValueTask SummaryAndRemarksAndParamAndReturns() { 130 | const string jsonConfig = """{}"""; 131 | const string moduleContent = """ 132 | /** 133 | * The example Summary 134 | * 135 | * @param a - a is not B 136 | * 137 | * @returns a string 138 | * 139 | * @remarks The example remark 140 | */ 141 | export function test(a: number): string; 142 | 143 | """; 144 | 145 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.d.ts", moduleContent); 146 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 147 | await Assert.That(diagnostics).IsEmpty(); 148 | 149 | await Assert.That(result.Length).IsEqualTo(4); 150 | string itsRuntimeModule = result[2]; 151 | await Verify(itsRuntimeModule); 152 | } 153 | 154 | [Test] 155 | public async ValueTask SummaryAndRemarksAndParamAndReturns_JSDocs() { 156 | const string jsonConfig = """{}"""; 157 | const string moduleContent = """ 158 | /** 159 | * The example Summary 160 | * 161 | * @param {number} a - a is not B 162 | * 163 | * @returns {string} a string 164 | * 165 | * @remarks The example remark 166 | */ 167 | export function test(a) { } 168 | 169 | """; 170 | 171 | (string path, string content) module = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/module.js", moduleContent); 172 | string[] result = jsonConfig.GenerateSourceText([module], out _, out ImmutableArray diagnostics); 173 | await Assert.That(diagnostics).IsEmpty(); 174 | 175 | await Assert.That(result.Length).IsEqualTo(4); 176 | string itsRuntimeModule = result[2]; 177 | await Verify(itsRuntimeModule); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/TSRuntimeGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | using Microsoft.Extensions.ObjectPool; 4 | using System.Collections.Immutable; 5 | using System.Diagnostics; 6 | using System.Text; 7 | using TSRuntime.Configs; 8 | using TSRuntime.Generation; 9 | using TSRuntime.Parsing; 10 | using ConfigOrError = (TSRuntime.Configs.Config? config, Microsoft.CodeAnalysis.Diagnostic? error); 11 | 12 | namespace TSRuntime; 13 | 14 | [Generator(LanguageNames.CSharp)] 15 | public sealed class TSRuntimeGenerator : IIncrementalGenerator { 16 | private readonly ObjectPool stringBuilderPool = new DefaultObjectPoolProvider().CreateStringBuilderPool(initialCapacity: 8192, maximumRetainedCapacity: 1024 * 1024); 17 | 18 | public void Initialize(IncrementalGeneratorInitializationContext context) { 19 | IncrementalValueProvider configProvider = context.AdditionalTextsProvider 20 | .Where((AdditionalText textFile) => textFile.Path.EndsWith("tsruntime.json")) 21 | .Collect() 22 | .Select((ImmutableArray textFiles, CancellationToken cancellationToken) => { 23 | if (textFiles.Length == 0) 24 | return (string.Empty, string.Empty, DiagnosticErrors.CreateNoConfigFileError()); 25 | if (textFiles.Length >= 2) 26 | return (string.Empty, string.Empty, DiagnosticErrors.CreateMultipleConfigFilesError()); 27 | 28 | AdditionalText textFile = textFiles[0]; 29 | string configPath = Path.GetDirectoryName(textFile.Path); 30 | 31 | SourceText? configContent = textFile.GetText(cancellationToken); 32 | if (configContent is null) 33 | return (string.Empty, string.Empty, DiagnosticErrors.CreateFileReadingError(textFile.Path)); 34 | 35 | return (configPath, configContent.ToString(), (Diagnostic?)null); 36 | }) 37 | .Select(((string path, string content, Diagnostic? error) file, CancellationToken cancellationToken) => file.error switch { 38 | null => (new Config(file.path, file.content), (Diagnostic?)null), 39 | _ => ((Config?)null, file.error) 40 | }); 41 | 42 | 43 | IncrementalValuesProvider<(TSFile? file, string content, Config? config)> fileList = context.AdditionalTextsProvider 44 | .Combine(configProvider) 45 | .Select<(AdditionalText, ConfigOrError), (TSFile?, string, Config?)>(((AdditionalText textFile, ConfigOrError configOrError) parameters, CancellationToken cancellationToken) => { 46 | if (parameters.configOrError.error is not null) 47 | return (null, string.Empty, null); 48 | 49 | Config config = parameters.configOrError.config!; 50 | AdditionalText textFile = parameters.textFile; 51 | 52 | string modulePath = textFile.Path.Replace('\\', '/'); 53 | if (!modulePath.StartsWith(config.WebRootPath)) 54 | return (null, string.Empty, config); 55 | modulePath = modulePath[config.WebRootPath.Length..]; 56 | 57 | foreach (InputPath inputPath in config.InputPath) 58 | if (inputPath.IsIncluded(modulePath)) { 59 | if (inputPath.ModuleFiles) { 60 | TSModule module = new(modulePath, inputPath.ModulePath, config.ErrorList); 61 | 62 | SourceText? content = textFile.GetText(cancellationToken); 63 | if (content is null) { 64 | Diagnostic error = DiagnosticErrors.CreateFileReadingError(textFile.Path); 65 | return (module, string.Empty, config); 66 | } 67 | 68 | return (module, content.ToString(), config); 69 | } 70 | else { 71 | TSScript script = new(modulePath, config.ErrorList); 72 | 73 | SourceText? content = textFile.GetText(cancellationToken); 74 | if (content is null) { 75 | Diagnostic error = DiagnosticErrors.CreateFileReadingError(textFile.Path); 76 | return (script, string.Empty, config); 77 | } 78 | 79 | return (script, content.ToString(), config); 80 | } 81 | } 82 | 83 | return (null, string.Empty, config); 84 | }); 85 | 86 | IncrementalValuesProvider<(TSModule, Config)> moduleList = fileList 87 | .Where(((TSFile? file, string content, Config? config) source) => source.file is TSModule) 88 | .Select(((TSFile? file, string content, Config? config) source, CancellationToken _) => { 89 | Debug.Assert(source.file is TSModule && source.config is not null); 90 | TSModule module = (TSModule)source.file!; 91 | Config config = source.config!; 92 | 93 | TSModule moduleWithFunctionsParsed = new(module.FilePath, module.URLPath, module.Name, TSFunction.ParseFile(source.content, isModule: true, config, module.FilePath)); 94 | return (moduleWithFunctionsParsed, config); 95 | }); 96 | 97 | IncrementalValuesProvider<(TSScript, Config)> scriptList = fileList 98 | .Where(((TSFile? file, string content, Config? config) source) => source.file is TSScript) 99 | .Select(((TSFile? file, string content, Config? config) source, CancellationToken _) => { 100 | Debug.Assert(source.file is TSScript && source.config is not null); 101 | TSScript script = (TSScript)source.file!; 102 | Config config = source.config!; 103 | 104 | TSScript scriptWithFunctionsParsed = new(script.FilePath, script.URLPath, script.Name, TSFunction.ParseFile(source.content, isModule: false, config, script.FilePath)); 105 | return (scriptWithFunctionsParsed, config); 106 | }); 107 | 108 | 109 | IncrementalValueProvider<(ImmutableArray moduleList, ConfigOrError configOrError)> moduleCollectionWithConfig = moduleList 110 | .Select(((TSModule module, Config config) tuple, CancellationToken _) => tuple.module) 111 | .Collect() 112 | .Combine(configProvider); 113 | 114 | IncrementalValueProvider<(ImmutableArray scriptList, (ImmutableArray moduleList, ConfigOrError configOrError) tuple)> scriptModuleCollectionWithConfig = scriptList 115 | .Select(((TSScript script, Config config) tuple, CancellationToken _) => tuple.script) 116 | .Collect() 117 | .Combine(moduleCollectionWithConfig); 118 | 119 | 120 | context.RegisterSourceOutput(scriptModuleCollectionWithConfig, stringBuilderPool.BuildClass); 121 | 122 | context.RegisterSourceOutput(configProvider, InterfaceCoreBuilder.BuildInterfaceCore); 123 | context.RegisterSourceOutput(moduleList, stringBuilderPool.BuildInterfaceModule); 124 | context.RegisterSourceOutput(scriptList, stringBuilderPool.BuildInterfaceScript); 125 | 126 | context.RegisterSourceOutput(moduleCollectionWithConfig, stringBuilderPool.BuildServiceExtension); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/CallbackTests/GeneratorCallbackTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Collections.Immutable; 3 | 4 | namespace TSRuntime.Tests; 5 | 6 | public sealed class GeneratorCallbackTests { 7 | private const string SCRIPT_PATH = $"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/site.d.ts"; 8 | 9 | [Test] 10 | public async ValueTask Parameterless() { 11 | const string jsonConfig = """{}"""; 12 | const string content = "export function callbackTest(someCallback: () => void): void;\n"; 13 | 14 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 15 | await Assert.That(diagnostics).IsEmpty(); 16 | 17 | string tsRuntime = result[0]; 18 | string itsRuntimeCore = result[1]; 19 | string itsRuntimeModule = result[2]; 20 | await Verify($""" 21 | --------- 22 | TSRuntime 23 | --------- 24 | 25 | {tsRuntime.XVersionNumber()} 26 | 27 | ---------- 28 | ITSRuntime 29 | ---------- 30 | 31 | {itsRuntimeCore.XVersionNumber()} 32 | 33 | ------ 34 | Module 35 | ------ 36 | 37 | {itsRuntimeModule} 38 | """); 39 | } 40 | 41 | [Test] 42 | public async ValueTask ParameterAndReturnType() { 43 | const string jsonConfig = """{}"""; 44 | const string content = "export function callbackTest(parseString: (str: string) => number): number;\n"; 45 | 46 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 47 | await Assert.That(diagnostics).IsEmpty(); 48 | 49 | string tsRuntime = result[0]; 50 | string itsRuntimeCore = result[1]; 51 | string itsRuntimeModule = result[2]; 52 | await Verify($""" 53 | --------- 54 | TSRuntime 55 | --------- 56 | 57 | {tsRuntime.XVersionNumber()} 58 | 59 | ---------- 60 | ITSRuntime 61 | ---------- 62 | 63 | {itsRuntimeCore.XVersionNumber()} 64 | 65 | ------ 66 | Module 67 | ------ 68 | 69 | {itsRuntimeModule} 70 | """); 71 | } 72 | 73 | [Test] 74 | public async ValueTask MultipleParameter() { 75 | const string jsonConfig = """{}"""; 76 | const string content = "export function callbackTest(a: boolean, parseString: (str: string) => number, b: number, callback2: (n: number) => string): void;\n"; 77 | 78 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 79 | await Assert.That(diagnostics).IsEmpty(); 80 | 81 | string tsRuntime = result[0]; 82 | string itsRuntimeCore = result[1]; 83 | string itsRuntimeModule = result[2]; 84 | await Verify($""" 85 | --------- 86 | TSRuntime 87 | --------- 88 | 89 | {tsRuntime.XVersionNumber()} 90 | 91 | ---------- 92 | ITSRuntime 93 | ---------- 94 | 95 | {itsRuntimeCore.XVersionNumber()} 96 | 97 | ------ 98 | Module 99 | ------ 100 | 101 | {itsRuntimeModule} 102 | """); 103 | } 104 | 105 | [Test] 106 | public async ValueTask Promise() { 107 | const string jsonConfig = """{}"""; 108 | const string content = "export function callbackTest(someCallback: () => Promise): void;\n"; 109 | 110 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 111 | await Assert.That(diagnostics).IsEmpty(); 112 | 113 | string tsRuntime = result[0]; 114 | string itsRuntimeCore = result[1]; 115 | string itsRuntimeModule = result[2]; 116 | await Verify($""" 117 | --------- 118 | TSRuntime 119 | --------- 120 | 121 | {tsRuntime.XVersionNumber()} 122 | 123 | ---------- 124 | ITSRuntime 125 | ---------- 126 | 127 | {itsRuntimeCore.XVersionNumber()} 128 | 129 | ------ 130 | Module 131 | ------ 132 | 133 | {itsRuntimeModule} 134 | """); 135 | } 136 | 137 | [Test] 138 | public async ValueTask PromiseVoid() { 139 | const string jsonConfig = """{}"""; 140 | const string content = "export function callbackTest(someCallback: () => Promise): void;\n"; 141 | 142 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 143 | await Assert.That(diagnostics).IsEmpty(); 144 | 145 | string tsRuntime = result[0]; 146 | string itsRuntimeCore = result[1]; 147 | string itsRuntimeModule = result[2]; 148 | await Verify($""" 149 | --------- 150 | TSRuntime 151 | --------- 152 | 153 | {tsRuntime.XVersionNumber()} 154 | 155 | ---------- 156 | ITSRuntime 157 | ---------- 158 | 159 | {itsRuntimeCore.XVersionNumber()} 160 | 161 | ------ 162 | Module 163 | ------ 164 | 165 | {itsRuntimeModule} 166 | """); 167 | } 168 | 169 | [Test] 170 | public async ValueTask Script() { 171 | const string jsonConfig = """ 172 | { 173 | "input path": { 174 | "include": "/", 175 | "module files": false 176 | } 177 | } 178 | """; 179 | const string content = "function callbackTest(someCallback: () => void): void;\n"; 180 | 181 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 182 | await Assert.That(diagnostics).IsEmpty(); 183 | 184 | string tsRuntime = result[0]; 185 | string itsRuntimeCore = result[1]; 186 | string itsRuntimeModule = result[2]; 187 | await Verify($""" 188 | --------- 189 | TSRuntime 190 | --------- 191 | 192 | {tsRuntime.XVersionNumber()} 193 | 194 | ---------- 195 | ITSRuntime 196 | ---------- 197 | 198 | {itsRuntimeCore.XVersionNumber()} 199 | 200 | ------ 201 | Module 202 | ------ 203 | 204 | {itsRuntimeModule} 205 | """); 206 | } 207 | 208 | [Test] 209 | public async ValueTask ReturnTypeNotSupported() { 210 | const string jsonConfig = """{}"""; 211 | const string content = "export function callbackTest(): () => void;\n"; 212 | 213 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 214 | await Assert.That(diagnostics).IsEmpty(); 215 | 216 | string tsRuntime = result[0]; 217 | string itsRuntimeCore = result[1]; 218 | string itsRuntimeModule = result[2]; 219 | await Verify($""" 220 | --------- 221 | TSRuntime 222 | --------- 223 | 224 | {tsRuntime.XVersionNumber()} 225 | 226 | ---------- 227 | ITSRuntime 228 | ---------- 229 | 230 | {itsRuntimeCore.XVersionNumber()} 231 | 232 | ------ 233 | Module 234 | ------ 235 | 236 | {itsRuntimeModule} 237 | """); 238 | } 239 | 240 | [Test] 241 | public async ValueTask NestedNotSupported() { 242 | const string jsonConfig = """{}"""; 243 | const string content = "export function callbackTest(someCallback: (nestedCallback: () => void) => void): void;\n"; 244 | 245 | string[] result = jsonConfig.GenerateSourceText([(SCRIPT_PATH, content)], out _, out ImmutableArray diagnostics); 246 | await Assert.That(diagnostics).IsEmpty(); 247 | 248 | string tsRuntime = result[0]; 249 | string itsRuntimeCore = result[1]; 250 | string itsRuntimeModule = result[2]; 251 | await Verify($""" 252 | --------- 253 | TSRuntime 254 | --------- 255 | 256 | {tsRuntime.XVersionNumber()} 257 | 258 | ---------- 259 | ITSRuntime 260 | ---------- 261 | 262 | {itsRuntimeCore.XVersionNumber()} 263 | 264 | ------ 265 | Module 266 | ------ 267 | 268 | {itsRuntimeModule} 269 | """); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/InputPathTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Collections.Immutable; 3 | using TSRuntime.Configs; 4 | 5 | namespace TSRuntime.Tests; 6 | 7 | public sealed class InputPathTests { 8 | private static readonly (string path, string content) testModule = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/TestModule.d.ts", "export declare function Test(a: number, b: string): number;\n"); 9 | private static readonly (string path, string content) nestedTestModule = ($"{GenerateSourceTextExtension.CONFIG_FOLDER_PATH}/NestedFolder/NestedTestModule.d.ts", "export declare function NestedTest(): void;\n"); 10 | 11 | 12 | [Test] 13 | public async ValueTask ParsesEvery_d_ts_File_WhenInputPathEmpty() { 14 | const string jsonConfig = """ 15 | { 16 | "input path": "", 17 | "service extension": false 18 | } 19 | """; 20 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 21 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 22 | 23 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs", "ITSRuntime_TestModule.g.cs", "ITSRuntime_NestedTestModule.g.cs"]); 24 | } 25 | 26 | [Test] 27 | public async ValueTask ParsesEvery_d_ts_File_WhenInputPathSlash() { 28 | const string jsonConfig = """ 29 | { 30 | "input path": "/", 31 | "service extension": false 32 | } 33 | """; 34 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 35 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 36 | 37 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs", "ITSRuntime_TestModule.g.cs", "ITSRuntime_NestedTestModule.g.cs"]); 38 | } 39 | 40 | [Test] 41 | public async ValueTask FilterExcludesFolder() { 42 | const string jsonConfig = """ 43 | { 44 | "input path": { 45 | "include": "", 46 | "excludes": "" 47 | }, 48 | "service extension": false 49 | } 50 | """; 51 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 52 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 53 | 54 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs"]); 55 | } 56 | 57 | [Test] 58 | public async ValueTask FilterExcludesFile() { 59 | const string jsonConfig = """ 60 | { 61 | "input path": { 62 | "include": "", 63 | "excludes": "/TestModule.d.ts" 64 | }, 65 | "service extension": false 66 | } 67 | """; 68 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 69 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 70 | 71 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs", "ITSRuntime_NestedTestModule.g.cs"]); 72 | } 73 | 74 | [Test] 75 | public async ValueTask FilterExcludesFileAndFolder() { 76 | const string jsonConfig = """ 77 | { 78 | "input path": { 79 | "include": "", 80 | "excludes": ["/TestModule.d.ts", "/NestedFolder/"] 81 | }, 82 | "service extension": false 83 | } 84 | """; 85 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 86 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 87 | 88 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs"]); 89 | } 90 | 91 | [Test] 92 | public async ValueTask MultipleIncludes() { 93 | const string jsonConfig = """ 94 | { 95 | "input path": ["/TestModule.d.ts", "/NestedFolder/NestedTestModule.d.ts"], 96 | "service extension": false 97 | } 98 | """; 99 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 100 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 101 | 102 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs", "ITSRuntime_TestModule.g.cs", "ITSRuntime_NestedTestModule.g.cs"]); 103 | } 104 | 105 | [Test] 106 | public async ValueTask ExcludeFolderDoesNotExcludeFile() { 107 | const string jsonConfig = """ 108 | { 109 | "input path": { 110 | "include": "/TestModule.d.ts", 111 | "excludes": ["/TestModule"] 112 | }, 113 | "service extension": false 114 | } 115 | """; 116 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 117 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 118 | 119 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs", "ITSRuntime_TestModule.g.cs"]); 120 | } 121 | 122 | [Test] 123 | public async ValueTask ExcludesAreScopedToSingle() { 124 | const string jsonConfig = """ 125 | { 126 | "input path": [{ 127 | "include": "", 128 | "excludes": ["/TestModule.d.ts", "/NestedFolder/NestedTestModule.d.ts"] 129 | }, 130 | ""], 131 | "service extension": false 132 | } 133 | """; 134 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 135 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 136 | 137 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs", "ITSRuntime_TestModule.g.cs", "ITSRuntime_NestedTestModule.g.cs"]); 138 | } 139 | 140 | [Test] 141 | public async ValueTask WrongFilePathIsIgnored() { 142 | const string jsonConfig = """ 143 | { 144 | "input path": "/TModule.d.ts", 145 | "service extension": false 146 | } 147 | """; 148 | ImmutableArray result = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out _); 149 | IEnumerable hintNames = result.Select((GeneratedSourceResult source) => source.HintName); 150 | 151 | await Assert.That(hintNames).IsEquivalentTo(["TSRuntime.g.cs", "ITSRuntime_Core.g.cs"]); 152 | } 153 | 154 | [Test] 155 | public async ValueTask ModulePath() { 156 | const string jsonConfig = """ 157 | { 158 | "input path": { 159 | "include": "/TestModule.d.ts", 160 | "module path" : "/site.js" 161 | }, 162 | "service extension": false 163 | } 164 | """; 165 | string[] result = jsonConfig.GenerateSourceText([testModule, nestedTestModule], out _, out _); 166 | string tsRuntime = result[0]; 167 | 168 | await Assert.That(tsRuntime).Contains("""_ => siteModule = jsRuntime.InvokeAsync("import", cancellationTokenSource.Token, "/site.js").AsTask()"""); 169 | } 170 | 171 | [Test] 172 | public async ValueTask ModulePathEmpty() { 173 | const string jsonConfig = """ 174 | { 175 | "input path": { 176 | "include": "/TestModule.d.ts", 177 | "module path" : "" 178 | }, 179 | "service extension": false 180 | } 181 | """; 182 | string[] result = jsonConfig.GenerateSourceText([testModule, nestedTestModule], out _, out _); 183 | string tsRuntime = result[0]; 184 | 185 | await Assert.That(tsRuntime).Contains("""_ => Module = jsRuntime.InvokeAsync("import", cancellationTokenSource.Token, "/").AsTask()"""); 186 | } 187 | 188 | [Test] 189 | public async ValueTask ModulePathOnlySlash() { 190 | const string jsonConfig = """ 191 | { 192 | "input path": { 193 | "include": "/TestModule.d.ts", 194 | "module path" : "/" 195 | }, 196 | "service extension": false 197 | } 198 | """; 199 | string[] result = jsonConfig.GenerateSourceText([testModule, nestedTestModule], out _, out _); 200 | string tsRuntime = result[0]; 201 | 202 | await Assert.That(tsRuntime).Contains("""_ => Module = jsRuntime.InvokeAsync("import", cancellationTokenSource.Token, "/").AsTask()"""); 203 | } 204 | 205 | [Test] 206 | public async ValueTask IncludeFolderWithModulePath_HasConflictingHintNames() { 207 | const string jsonConfig = """ 208 | { 209 | "input path": { 210 | "include": "", 211 | "module path" : "/site.js" 212 | }, 213 | "service extension": false 214 | } 215 | """; 216 | _ = jsonConfig.GenerateSourceResult([testModule, nestedTestModule], out _, out ImmutableArray diagnostics); 217 | 218 | await Assert.That(diagnostics).HasSingleItem(); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/DiagnosticErrors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace TSRuntime; 4 | 5 | public static class DiagnosticErrors { 6 | public static Diagnostic CreateNoConfigFileError() 7 | => Diagnostic.Create(NoConfigFile, null); 8 | 9 | private static DiagnosticDescriptor NoConfigFile { get; } = new( 10 | id: "BTS001", 11 | title: "missing config file", 12 | messageFormat: "No tsruntime.json file found. Make sure the file ends with 'tsruntime.json' and is added with the directive.", 13 | category: "Blazor.TSRuntime", 14 | DiagnosticSeverity.Error, 15 | isEnabledByDefault: true); 16 | 17 | public static Diagnostic CreateMultipleConfigFilesError() 18 | => Diagnostic.Create(MultipleConfigFiles, null); 19 | 20 | private static DiagnosticDescriptor MultipleConfigFiles { get; } = new( 21 | id: "BTS002", 22 | title: "multiple config files", 23 | messageFormat: "multiple tsruntime.json files found. Make sure only 1 file ends with 'tsruntime.json'", 24 | category: "Blazor.TSRuntime", 25 | DiagnosticSeverity.Error, 26 | isEnabledByDefault: true); 27 | 28 | public static Diagnostic CreateFileReadingError(string textFilePath) 29 | => Diagnostic.Create(FileReadingError, null, [textFilePath]); 30 | 31 | private static DiagnosticDescriptor FileReadingError { get; } = new( 32 | id: "BTS003", 33 | title: "file reading error", 34 | messageFormat: "File reading error: '{0}' could not be accessed for reading.", 35 | category: "Blazor.TSRuntime", 36 | DiagnosticSeverity.Error, 37 | isEnabledByDefault: true); 38 | 39 | 40 | 41 | #region Config 42 | 43 | public static void AddConfigInvalidError(this List errorList) 44 | => errorList.Add(Diagnostic.Create(ConfigInvalid, null)); 45 | 46 | private static DiagnosticDescriptor ConfigInvalid { get; } = new( 47 | id: "BTS004", 48 | title: "config is invalid json", 49 | messageFormat: "config is in an invalid json format", 50 | category: "Blazor.TSRuntime", 51 | DiagnosticSeverity.Error, 52 | isEnabledByDefault: true); 53 | 54 | 55 | public static void AddConfigKeyNotFoundError(this List errorList, string jsonKey) 56 | => errorList.Add(Diagnostic.Create(ConfigKeyNotFound, null, [jsonKey])); 57 | 58 | private static DiagnosticDescriptor ConfigKeyNotFound { get; } = new( 59 | id: "BTS005", 60 | title: "config key not found", 61 | messageFormat: "invalid config: key '{0}' not found, default is taken instead", 62 | category: "Blazor.TSRuntime", 63 | DiagnosticSeverity.Warning, 64 | isEnabledByDefault: true); 65 | 66 | 67 | public static void AddConfigUnexpectedTypeError(this List errorList, string jsonKey) 68 | => errorList.Add(Diagnostic.Create(ConfigUnexpectedType, null, [jsonKey])); 69 | 70 | private static DiagnosticDescriptor ConfigUnexpectedType { get; } = new( 71 | id: "BTS006", 72 | title: "config unexpected type", 73 | messageFormat: "invalid config: '{0}' has unexpected type, default is taken instead", 74 | category: "Blazor.TSRuntime", 75 | DiagnosticSeverity.Warning, 76 | isEnabledByDefault: true); 77 | 78 | 79 | public static void AddConfigStringExpectedError(this List errorList, string jsonKey) 80 | => errorList.Add(Diagnostic.Create(ConfigStringExpected, null, [jsonKey])); 81 | 82 | private static DiagnosticDescriptor ConfigStringExpected { get; } = new( 83 | id: "BTS007", 84 | title: "config string expected", 85 | messageFormat: "invalid config: '{0}' has wrong type, must be a string, default is taken instead. If you want to have null, use string literal \"null\" instead.", 86 | category: "Blazor.TSRuntime", 87 | DiagnosticSeverity.Warning, 88 | isEnabledByDefault: true); 89 | 90 | 91 | public static void AddConfigBoolExpectedError(this List errorList, string jsonKey) 92 | => errorList.Add(Diagnostic.Create(ConfigBoolExpected, null, [jsonKey])); 93 | 94 | private static DiagnosticDescriptor ConfigBoolExpected { get; } = new( 95 | id: "BTS008", 96 | title: "config bool expected", 97 | messageFormat: "invalid config: '{0}' has wrong type, must be either \"true\" or \"false\", default is taken instead", 98 | category: "Blazor.TSRuntime", 99 | DiagnosticSeverity.Warning, 100 | isEnabledByDefault: true); 101 | 102 | 103 | public static void AddConfigNamePatternMissingEndTagError(this List errorList) 104 | => errorList.Add(Diagnostic.Create(ConfigNamePatternMissingEndTag, null)); 105 | 106 | private static DiagnosticDescriptor ConfigNamePatternMissingEndTag { get; } = new( 107 | id: "BTS009", 108 | title: "config name pattern missing '#'", 109 | messageFormat: "invalid config: name pattern has starting '#' but missing closing '#'", 110 | category: "Blazor.TSRuntime", 111 | DiagnosticSeverity.Warning, 112 | isEnabledByDefault: true); 113 | 114 | 115 | public static void AddConfigNamePatternInvalidVariableError(this List errorList, string invalidVariable, string[] validVaraibleNames) 116 | => errorList.Add(Diagnostic.Create(ConfigNamePatternInvalidVariable, null, [invalidVariable, string.Join("\", \"", validVaraibleNames)])); 117 | 118 | private static DiagnosticDescriptor ConfigNamePatternInvalidVariable { get; } = new( 119 | id: "BTS010", 120 | title: "config nametransform expected", 121 | messageFormat: "invalid config: name pattern has invalid variable \"{0}\". Allowed values are: \"{1}\"", 122 | category: "Blazor.TSRuntime", 123 | DiagnosticSeverity.Warning, 124 | isEnabledByDefault: true); 125 | 126 | 127 | public static void AddConfigNameTransformExpectedError(this List errorList, string jsonKey) 128 | => errorList.Add(Diagnostic.Create(ConfigNameTransformExpected, null, [jsonKey])); 129 | 130 | private static DiagnosticDescriptor ConfigNameTransformExpected { get; } = new( 131 | id: "BTS011", 132 | title: "config nametransform expected", 133 | messageFormat: "invalid config: '{0}' has wrong value, must be either \"first upper case\", \"first lower case\", \"upper case\", \"lower case\" or \"none\"", 134 | category: "Blazor.TSRuntime", 135 | DiagnosticSeverity.Warning, 136 | isEnabledByDefault: true); 137 | 138 | 139 | public static void AddConfigFunctionTransformMissingActionError(this List errorList, string jsonKey) 140 | => errorList.Add(Diagnostic.Create(ConfigFunctionTransformMissingAction, null, [jsonKey])); 141 | 142 | private static DiagnosticDescriptor ConfigFunctionTransformMissingAction { get; } = new( 143 | id: "BTS012", 144 | title: "config function transform missing action", 145 | messageFormat: "malformed config: '{0}' should contain '#action#' when 2 or more method types are enabled, otherwise it leads to duplicate method naming", 146 | category: "Blazor.TSRuntime", 147 | DiagnosticSeverity.Warning, 148 | isEnabledByDefault: true); 149 | 150 | 151 | public static void AddInputPathNoStartingSlashError(this List errorList, string jsonKey) 152 | => errorList.Add(Diagnostic.Create(InputPathNoStartingSlash, null, [jsonKey])); 153 | 154 | private static DiagnosticDescriptor InputPathNoStartingSlash { get; } = new( 155 | id: "BTS013", 156 | title: "config 'input path' has no starting slash", 157 | messageFormat: "malformed config: '{0}' should start with '/'", 158 | category: "Blazor.TSRuntime", 159 | DiagnosticSeverity.Warning, 160 | isEnabledByDefault: true); 161 | 162 | 163 | public static void AddModulePathNoJsExtensionError(this List errorList, string jsonKey) 164 | => errorList.Add(Diagnostic.Create(ModulePathNoJsExtension, null, [jsonKey])); 165 | 166 | private static DiagnosticDescriptor ModulePathNoJsExtension { get; } = new( 167 | id: "BTS014", 168 | title: "config 'module path' has no '.js' extension", 169 | messageFormat: "malformed config: '{0}' should end with '.js'", 170 | category: "Blazor.TSRuntime", 171 | DiagnosticSeverity.Warning, 172 | isEnabledByDefault: true); 173 | 174 | #endregion 175 | 176 | 177 | #region Parsing 178 | 179 | public static void AddFunctionParseError(this List errorList, DiagnosticDescriptor descriptor, string filePath, int lineNumber, int position) 180 | => errorList.Add(Diagnostic.Create(descriptor, null, [filePath, lineNumber, position])); 181 | 182 | public static DiagnosticDescriptor FileMissingOpenBracket { get; } = new( 183 | id: "BTS015", 184 | title: "invalid file: missing '('", 185 | messageFormat: "invalid file: '{0}' at line {1}: missing '(' after column {2} (the token that indicates the start of function parameters)", 186 | category: "Blazor.TSRuntime", 187 | DiagnosticSeverity.Warning, 188 | isEnabledByDefault: true); 189 | 190 | public static DiagnosticDescriptor FileMissingClosingGenericBracket { get; } = new( 191 | id: "BTS016", 192 | title: "invalid file: missing '('", 193 | messageFormat: "invalid file: '{0}' at line {1}: missing '>' after column {2} (the token that marks the end of generics)", 194 | category: "Blazor.TSRuntime", 195 | DiagnosticSeverity.Warning, 196 | isEnabledByDefault: true); 197 | 198 | public static DiagnosticDescriptor FileNoParameterEnd { get; } = new( 199 | id: "BTS017", 200 | title: "invalid file: no end of parameter", 201 | messageFormat: "invalid file: '{0}' at line {1}: missing ')' after column {2} (the token that marks end of parameters)", 202 | category: "Blazor.TSRuntime", 203 | DiagnosticSeverity.Warning, 204 | isEnabledByDefault: true); 205 | 206 | #endregion 207 | } 208 | -------------------------------------------------------------------------------- /Blazor.TSRuntime/Parsing/TSParameter.cs: -------------------------------------------------------------------------------- 1 | namespace TSRuntime.Parsing; 2 | 3 | /// 4 | /// Represents a parameter inside a . 5 | /// 6 | public record struct TSParameter() : IEquatable { 7 | /// 8 | /// Description of this parameter. 9 | /// 10 | public string summary = string.Empty; 11 | 12 | /// 13 | /// The given name of the parameter. 14 | /// 15 | public string name = string.Empty; 16 | 17 | /// 18 | /// The js-type of the parameter/array. 19 | /// If null, should be used and holds at least one item. 20 | /// 21 | public string? type = string.Empty; 22 | 23 | /// 24 | /// The js-type of the parameter when it is a callback. 25 | /// The last item is the returnType. 26 | /// If empty, should be used. 27 | /// 28 | public TSParameter[] typeCallback = []; 29 | 30 | /// 31 | /// If this parameter is a callback (if is null), this value indicates if the returnType of that callback is a Promise. 32 | /// 33 | public bool typeCallbackPromise = false; 34 | 35 | /// 36 | /// Indicates if the type may be null. 37 | /// 38 | public bool typeNullable = false; 39 | 40 | /// 41 | /// Indicates if the given parameter is an array. 42 | /// 43 | public bool array = false; 44 | 45 | /// 46 | /// Indicates if the array itself may be null. 47 | /// 48 | public bool arrayNullable = false; 49 | 50 | /// 51 | /// Indicates if the parameter is optional 52 | /// 53 | public bool optional = false; 54 | 55 | 56 | /// 57 | /// Parses the name of the given subStr. 58 | /// 59 | /// 60 | public void ParseName(ReadOnlySpan subStr) { 61 | if (subStr is [.., '?']) { 62 | optional = true; 63 | name = subStr[..^1].ToString(); 64 | } 65 | else 66 | name = subStr.ToString(); 67 | } 68 | 69 | 70 | /// 71 | /// Parses the type of the given subStr. 72 | /// 73 | /// e.g.
74 | /// - number
75 | /// - number | null
76 | /// - number | undefined
77 | /// - number[]
78 | /// - Array<number>
79 | /// - (number | null)[]
80 | /// - (number | null)[] | null 81 | ///
82 | ///
83 | /// Only the part of the string that represents the type of a parameter (starting after ": " and ending before ',' or ')'. 84 | public void ParseType(ReadOnlySpan subStr) { 85 | if (subStr is ['r', 'e', 'a', 'd', 'o', 'n', 'l', 'y', ' ', ..]) { 86 | subStr = subStr[9..]; 87 | subStr = subStr.TrimStart(); 88 | } 89 | 90 | int arrowIndex; 91 | { 92 | arrowIndex = -1; 93 | int bracketCount = 0; 94 | for (int i = 0; i < subStr.Length - 1; i++) 95 | switch (subStr[i]) { 96 | case '(': 97 | bracketCount++; 98 | break; 99 | case ')': 100 | bracketCount--; 101 | break; 102 | case '=': 103 | if (bracketCount == 0) 104 | if (subStr[i + 1] is '>') { 105 | arrowIndex = i; 106 | goto arrowIndex_double_break; 107 | } 108 | break; 109 | } 110 | } 111 | arrowIndex_double_break: 112 | 113 | if (arrowIndex != -1) { 114 | type = null; 115 | 116 | ReadOnlySpan parameterStr = subStr[..arrowIndex].TrimEnd(); 117 | 118 | if (parameterStr is not ['(', .., ')']) 119 | return; 120 | parameterStr = parameterStr[1..^1].Trim(); // cut "(..)" 121 | 122 | 123 | List parameterList = []; 124 | 125 | // arrow function parameters 126 | while (parameterStr.Length > 0) { 127 | TSParameter tsParameter = new(); 128 | 129 | // parse name 130 | int colonIndex = parameterStr.IndexOfAny([':']); 131 | if (colonIndex != -1) { 132 | tsParameter.ParseName(parameterStr[..colonIndex].TrimEnd()); 133 | parameterStr = parameterStr[(colonIndex + 1)..].TrimStart(); 134 | } 135 | 136 | // parse type 137 | int parameterTypeEnd; 138 | { 139 | int bracketCount = 0; 140 | int i; 141 | for (i = 0; i < parameterStr.Length; i++) 142 | switch (parameterStr[i]) { 143 | case '(' or '[' or '<': 144 | bracketCount++; 145 | break; 146 | case ')' or ']' or '>': 147 | bracketCount--; 148 | break; 149 | case ',': 150 | if (bracketCount <= 0) 151 | goto double_break; 152 | break; 153 | } 154 | double_break: 155 | parameterTypeEnd = i; 156 | } 157 | 158 | tsParameter.ParseType(parameterStr[..parameterTypeEnd].TrimEnd()); 159 | if (parameterTypeEnd < parameterStr.Length) 160 | parameterStr = parameterStr[(parameterTypeEnd + 1)..]; 161 | else 162 | parameterStr = []; 163 | 164 | parameterList.Add(tsParameter); 165 | } 166 | 167 | // arrow function returnType 168 | TSParameter returnType = new() { name = "ReturnValue", type = "void" }; 169 | ReadOnlySpan returnStr = subStr[(arrowIndex + 2)..].TrimStart(); // skip "=>" 170 | 171 | typeCallbackPromise = returnStr is ['P', 'r', 'o', 'm', 'i', 's', 'e', '<', ..]; 172 | if (typeCallbackPromise) { 173 | int closingBracket = returnStr.LastIndexOf('>'); 174 | if (closingBracket != -1) 175 | returnStr = returnStr[8..closingBracket].Trim(); 176 | else 177 | returnStr = returnStr[8..].Trim(); 178 | } 179 | returnType.ParseType(returnStr); 180 | 181 | 182 | typeCallback = [.. parameterList, returnType]; 183 | } 184 | else { 185 | // array or type 186 | (typeNullable, bool isOptional) = ParseNullUndefined(ref subStr); 187 | optional |= isOptional; 188 | 189 | if (subStr is [.., ']']) { 190 | ReadOnlySpan view = subStr[..^1].TrimEnd(); 191 | if (view is [.., '[']) { 192 | subStr = view[..^1].TrimEnd(); 193 | array = true; 194 | arrayNullable = typeNullable; 195 | 196 | if (subStr is ['(', .., ')']) { 197 | subStr = subStr[1..^1].Trim(); // cut "(..)" 198 | (bool nullable, bool optional) = ParseNullUndefined(ref subStr); 199 | typeNullable = nullable | optional; 200 | } 201 | else 202 | typeNullable = false; 203 | 204 | type = subStr.ToString(); 205 | return; 206 | } 207 | } 208 | 209 | if (subStr is ['A', 'r', 'r', 'a', 'y', '<', .., '>']) { 210 | array = true; 211 | arrayNullable = typeNullable; 212 | subStr = subStr[6..^1].Trim(); // cut "Array<..>" 213 | 214 | (bool nullable, bool optional) = ParseNullUndefined(ref subStr); 215 | typeNullable = nullable | optional; 216 | 217 | type = subStr.ToString(); 218 | return; 219 | } 220 | 221 | type = subStr.ToString(); 222 | 223 | 224 | static (bool nullable, bool optional) ParseNullUndefined(ref ReadOnlySpan subStr) { 225 | if (IsNullable(ref subStr)) 226 | return (true, IsUndefinedable(ref subStr)); 227 | else if (IsUndefinedable(ref subStr)) 228 | return (IsNullable(ref subStr), true); 229 | 230 | return (false, false); 231 | 232 | 233 | static bool IsNullable(ref ReadOnlySpan subStr) { 234 | if (subStr is ['n', 'u', 'l', 'l', ..]) { 235 | ReadOnlySpan view = subStr[4..]; 236 | view = view.TrimStart(); 237 | if (view is ['|', ..]) { 238 | subStr = view[1..].TrimStart(); 239 | return true; 240 | } 241 | } 242 | 243 | if (subStr is [.., 'n', 'u', 'l', 'l']) { 244 | ReadOnlySpan view = subStr[..^4]; 245 | view = view.TrimEnd(); 246 | if (view is [.., '|']) { 247 | subStr = view[..^1].TrimEnd(); 248 | return true; 249 | } 250 | } 251 | 252 | return false; 253 | } 254 | 255 | static bool IsUndefinedable(ref ReadOnlySpan subStr) { 256 | if (subStr is ['u', 'n', 'd', 'e', 'f', 'i', 'n', 'e', 'd', ..]) { 257 | ReadOnlySpan view = subStr[9..]; 258 | view = view.TrimStart(); 259 | if (view is ['|', ..]) { 260 | subStr = view[1..].TrimStart(); 261 | return true; 262 | } 263 | } 264 | 265 | if (subStr is [.., 'u', 'n', 'd', 'e', 'f', 'i', 'n', 'e', 'd']) { 266 | ReadOnlySpan view = subStr[..^9]; 267 | view = view.TrimEnd(); 268 | if (view is [.., '|']) { 269 | subStr = view[..^1].TrimEnd(); 270 | return true; 271 | } 272 | } 273 | 274 | return false; 275 | } 276 | } 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor.TSRuntime 2 | 3 | An improved JSRuntime with 4 | 5 | - automatic JS-module loading and caching 6 | - compile time errors instead of runtime errors 7 | - IntelliSense guidance 8 | 9 | ![InlineComposition Example](README_IMAGE.png) 10 | 11 | Works with [*JavaScript JSDoc*](#get-started) and [*TypeScript*](#get-started). 12 | 13 | 14 |

15 | ## Available Methods 16 | 17 | ### Invoke 18 | 19 | Each "export function" in JavaScript will generate up to 3 C#-methods: 20 | - **Invoke** - interops synchronous 21 | - **InvokeTrySync** - interops synchronous if possible, otherwise asynchronous 22 | - **InvokeAsync** - interops asynchronous 23 | 24 | ```csharp 25 | // saveNumber(name: string, myNumber: number) 26 | 27 | TsRuntime.SaveNumberInvoke("key1", 5); // will invoke sync 28 | await TsRuntime.SaveNumberInvokeTrySync("key1", 5); // invokes sync if possible, otherwise async 29 | await TsRuntime.SaveNumberInvokeAsync("key1", 5); // invokes async 30 | ``` 31 | 32 | **Note**: 33 | - *InvokeTrySync* checks if IJSInProcessRuntime is available and if available, executes the call synchronous. 34 | So, if the module is already be downloaded and IJSInProcessRuntime is available, this method executes synchronous. 35 | - Asynchronous JavaScript-functions (JS-functions that return a promise) should be called with *InvokeAsync* (not *Invoke* or *InvokeTrySync*), otherwise the promise will not be awaited. 36 | - *Invoke*-interop fails with an exception when module is not loaded. 37 | So make sure to await the corresponding preload-method beforehand. 38 | 39 | ### Preload 40 | 41 | Each module will generate a method to preload the module. 42 | Additionaly, there is a *PreloadAllModules* method, that preloads all modules. 43 | Preloading will start the download of the JS-module and the task completes when the module is downloaded and cached. 44 | If a JS-function is called before or while preloading, the download task will first be awaited before executing the function (A sync-call throws an exception). 45 | Therefore, it is recommended to call this method as "fire and forget". 46 | ```csharp 47 | _ = PreloadExample(); // loads and caches Example module in the background 48 | _ = PreloadAllModules(); // loads and caches all modules in the background 49 | await PreloadAllModules(); // awaits the loading of all modules, recommended when using sync-interop 50 | ``` 51 | 52 | Furthermore you can prefetch your modules on page load, so the Preload-methods will only get a reference to the module. 53 | ```html 54 | 55 | ... 56 | 57 | 58 | ``` 59 | 60 | 61 |

62 | ## Get Started 63 | 64 | ### 1. Add NuGet package 65 | 66 | In your .csproj-file put a package reference to *Blazor.TSRuntime*. 67 | 68 | ```xml 69 | 70 | 71 | 72 | ``` 73 | 74 | 75 | ### 2. Add <AdditionalFiles> 76 | 77 | In your .csproj-file put an <AdditionalFiles> directive to *tsruntime.json* 78 | and an <AdditionalFiles> to make all .js-files available to the source-generator. 79 | 80 | ```xml 81 | 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | Create a *tsruntime.json*-file in the same folder as your .csproj-file. 89 | 90 | ```json 91 | { 92 | "invoke function": { 93 | "sync enabled": false, 94 | "trysync enabled": true, 95 | "async enabled": false, 96 | "name pattern": { 97 | "pattern": "#function#", 98 | "module transform": "first upper case", 99 | "function transform": "first upper case", 100 | "action transform": "none" 101 | }, 102 | "type map": { 103 | "number": { 104 | "type": "TNumber", 105 | "generic types": { 106 | "name": "TNumber", 107 | "constraint": "INumber" 108 | } 109 | }, 110 | "boolean": "bool", 111 | "Uint8Array": "byte[]", 112 | "HTMLElement": "ElementReference" 113 | } 114 | } 115 | } 116 | ``` 117 | 118 | 119 | ### 3. Register ITSRuntime 120 | 121 | If everything is set up correctly, the generator should already be generating the 2 files *TSRuntime*, *ITSRuntime*. 122 | Register them in your dependency container. 123 | 124 | ```csharp 125 | using Microsoft.JSInterop; 126 | 127 | // IServiceCollection services 128 | services.AddTSRuntime(); 129 | ``` 130 | 131 | ### 4. Hello World 132 | 133 | Now you are ready to rumble, to make a "Hello World" test you can create 2 files: 134 | 135 | - Example.razor 136 | 137 | ```razor 138 | 139 | 140 | @code { 141 | [Inject] 142 | public required ITSRuntime TsRuntime { private get; init; } 143 | 144 | private async Task InvokeJS() => await TsRuntime.Example(); 145 | } 146 | ``` 147 | 148 | - Example.razor.js 149 | 150 | ```js 151 | export function example() { 152 | console.log("Hello World"); 153 | } 154 | ``` 155 | 156 | 157 | ### Optional 158 | 159 | You can add a *jsconfig.json* file and rename **tsruntime.json** to **jsconfig.tsruntime.json**. 160 | Here is an example *jsconfig.json*: 161 | 162 | ```json 163 | { 164 | "compilerOptions": { 165 | "target": "es2022", 166 | "checkJs": true, 167 | "strictNullChecks": true, 168 | "noImplicitAny": true 169 | } 170 | } 171 | ``` 172 | 173 | 174 | ### TypeScript 175 | 176 | For using TypeScript, you only need a few adjustments: 177 | - *tsconfig.json* instead of *jsconfig.json* 178 | - rename *jsconfig.tsruntime.json* to *tsconfig.tsruntime.json* 179 | - change *<AdditionalFiles Include="\*\*\\\*.js"* /> to *<AdditionalFiles Include="\*\*\\\*.ts" />* 180 | 181 |

182 | Note: 183 | To recognize a module, the file must end with ".js", ".ts" or ".d.ts". 184 | Function definitions inside a module must start with "export function". 185 | Futhermore a function definition must not contain any line breaks. 186 | 187 | If using TypeScript types together with JSDoc types, JSDoc takes priority, 188 | because JSDoc is parsed after the function declaration and overwrites the previous type. 189 | But this problem should not exist in the first place as long you do not mix things up, use JS with JSDoc or TS with TSDoc. 190 | 191 | 192 |

193 | ## Config - tsruntime.json 194 | 195 | All available config keys with its default value: 196 | 197 | ```json 198 | { 199 | "webroot path": "", 200 | "input path": { 201 | "include": "/", 202 | "excludes": [ "/bin", "/obj", "/Properties" ], 203 | "module files": true 204 | }, 205 | "using statements": [ "Microsoft.AspNetCore.Components", "System.Numerics" ], 206 | "invoke function": { 207 | "sync enabled": false, 208 | "trysync enabled": true, 209 | "async enabled": false, 210 | "name pattern": { 211 | "pattern": "#function#", 212 | "module transform": "first upper case", 213 | "function transform": "first upper case", 214 | "action transform": "none", 215 | "action name": { 216 | "sync": "Invoke", 217 | "trysync": "InvokeTrySync", 218 | "async": "InvokeAsync" 219 | } 220 | }, 221 | "promise": { 222 | "only async enabled": true, 223 | "append async": false 224 | }, 225 | "type map": { 226 | "number": { 227 | "type": "TNumber", 228 | "generic types": { 229 | "name": "TNumber", 230 | "constraint": "INumber" 231 | } 232 | }, 233 | "boolean": "bool", 234 | "Uint8Array": "byte[]", 235 | "HTMLElement": "ElementReference" 236 | } 237 | }, 238 | "preload function": { 239 | "name pattern": { 240 | "pattern": "Preload#module#", 241 | "module transform": "first upper case" 242 | }, 243 | "all modules name": "PreloadAllModules", 244 | }, 245 | "module grouping": { 246 | "enabled": false, 247 | "interface name pattern": { 248 | "pattern": "I#module#Module", 249 | "module transform": "first upper case" 250 | } 251 | }, 252 | "js runtime": { 253 | "sync enabled": false, 254 | "trysync enabled": false, 255 | "async enabled": false 256 | }, 257 | "service extension": true 258 | } 259 | ``` 260 | 261 | - **[\[webroot path\]](Readme_md/InputPath.md)**: 262 | Relative path to the web root (starting folder 'wwwroot' is ignored). 263 | - **[\[input path\]](Readme_md/InputPath.md)**: 264 | Folder where to locate the input files. Path relative to [webroot path] and must start with '/'. 265 | - **[\[using statements\]](Readme_md/UsingStatements.md)**: 266 | List of generated using statements at the top of ITSRuntime. 267 | - **[\[invoke function\].\[sync enabled\]](#invoke)**: 268 | Toggles whether sync invoke methods should be generated for modules. 269 | - **[\[invoke function\].\[trysync enabled\]](#invoke)**: 270 | Toggles whether try-sync invoke methods should be generated for modules. 271 | - **[\[invoke function\].\[async enabled\]](#invoke)**: 272 | Toggles whether async invoke methods should be generated for modules. 273 | - **[\[invoke function\].\[name pattern\].\[pattern\]](Readme_md/NamePattern.md)**: 274 | Naming of the generated methods that invoke module functions. 275 | - **[\[invoke function\].\[name pattern\].\[module transform\]](Readme_md/NamePattern.md)**: 276 | Lower/Upper case transform for the variable #module#. 277 | - **[\[invoke function\].\[name pattern\].\[function transform\]](Readme_md/NamePattern.md)**: 278 | Lower/Upper case transform for the variable #function#. 279 | - **[\[invoke function\].\[name pattern\].\[action transform\]](Readme_md/NamePattern.md)**: 280 | Lower/Upper case transform for the variable #action#.. 281 | - **[\[invoke function\].\[name pattern\].\[action name\]\[sync\]](Readme_md/NamePattern.md)**: 282 | Naming of the #action# variable for the invoke module functions name pattern when the action is synchronous. 283 | - **[\[invoke function\].\[name pattern\].\[action name\]\[trysync\]](Readme_md/NamePattern.md)**: 284 | Naming of the #action# variable for the invoke module functions name pattern when the action is try synchronous. 285 | - **[\[invoke function\].\[name pattern\].\[action name\]\[async\]](Readme_md/NamePattern.md)**: 286 | Naming of the #action# variable for the invoke module functions name pattern when the action is asynchronous. 287 | - **[\[invoke function\].\[promise\].\[only async enabled\]](Readme_md/PromiseFunction.md)**: 288 | Generates only async invoke method when return-type is promise. 289 | - **[\[invoke function\].\[promise\].\[append async\]](Readme_md/PromiseFunction.md)**: 290 | Appends to the name 'Async' when return-type is promise. 291 | - **[\[invoke function\].\[type map\]](Readme_md/TypeMap.md)**: 292 | Mapping of TypeScript-types (key) to C#-types (value). Not listed types are mapped unchanged (Identity function). 293 | - **[\[preload function\].\[name pattern\].\[pattern\]](Readme_md/NamePattern.md)**: 294 | Naming of the generated methods that preloads a specific module. 295 | - **[\[preload function\].\[name pattern\].\[module transform\]](Readme_md/NamePattern.md)**: 296 | Lower/Upper case transform for the variable #module#. 297 | - **[\[preload function\].\[all modules name\]](Readme_md/NamePattern.md)**: 298 | Naming of the method that preloads all modules. 299 | - **[\[module grouping\].\[enabled\]](Readme_md/ModuleGrouping.md)**: 300 | Each module gets it own interface and the functions of that module are only available in that interface. 301 | - **[\[module grouping\].\[interface name pattern\].\[pattern\]](Readme_md/NamePattern.md)**: 302 | Naming of the generated module interfaces when *module grouping* is enabled. 303 | - **[\[module grouping\].\[interface name pattern\].\[module transform\]](Readme_md/NamePattern.md)**: 304 | Lower/Upper case transform for the variable #module#. 305 | - **[\[js runtime\].\[sync enabled\]](Readme_md/JSRuntime.md)**: 306 | Toggles whether generic JSRuntime sync invoke method should be generated. 307 | - **[\[js runtime\].\[trysync enabled\]](Readme_md/JSRuntime.md)**: 308 | Toggles whether generic JSRuntime try-sync invoke method should be generated. 309 | - **[\[js runtime\].\[async enabled\]](Readme_md/JSRuntime.md)**: 310 | Toggles whether generic JSRuntime async invoke method should be generated. 311 | - **[\[service extension\]](Readme_md/ModuleGrouping.md)**: 312 | A service extension method is generated, which registers ITSRuntime and if enabled, the module interfaces. 313 | 314 | 315 |

316 | ## Callback (Function as Parameter) 317 | 318 | ```js 319 | /** 320 | * @param {(key: string) => Promise} mapToId 321 | * @returns {Promise} 322 | */ 323 | export async function callbackExample(mapToId) { 324 | const id = await mapToId("42"); 325 | console.log(id); 326 | } 327 | ``` 328 | 329 | ```csharp 330 | // CallbackExample(Func mapToId) 331 | await TsRuntime.CallbackExample((string key) => ValueTask.FromResult(key.GetHashCode())); 332 | ``` 333 | 334 | In JavaScript functions are first-class citizens and a variable/parameter can hold a function. 335 | In C# the equivalent of that are delegates. 336 | Such variables are also called callbacks. 337 | When using a JS-function as parameter, it will be mapped automatically to the corresponding *Action<>*/*Func<>* type. 338 | However, behind the scenes there is a lot going on to make this work and there are a few edge cases you should be aware of. 339 | 340 | ### Sync/Async Callbacks 341 | 342 | To interop from C# to JS you can choose from 3 options: *Sync*/*TrySync*/*Async*. 343 | You may expect the same when using interop from JS to C#. 344 | Unfortunately, it is not implemented that way and you can only choose between *Sync* and *Async*: 345 | 346 | If the return-type is not a *Promise<T>*, it will be a *Sync* call. 347 | If the return-type is a *Promise<T>*, it will be *Async* call. 348 | 349 | So, to make sure it works in every environment, your callbacks should always return a *Promise<T>*. 350 | Note, in that case the return-type of your delegate will be *ValueTask*/*ValueTask<T>*. 351 | When your C# method itself is synchronous, just use *ValueTask.CompletedTask*/*ValueTask<T>.FromResult()* as return value. 352 | 353 | ### Callback Module 354 | 355 | To make the mapping possible, additional JS functions are needed. 356 | These JS functions are located in an additional module, the *callback*-module. 357 | This internal module loads automatically. 358 | For Sync-invoke scenarios, you must ensure that the used modules are loaded. 359 | There is no dedicated *Preload()*-method for the *callback*-module, 360 | but the *PreloadAll()*-method awaits also the *callback*-module. 361 | 362 | ### DotNetObjectReference 363 | 364 | For the mapping a *DotNetObjectReference* is created. 365 | To make sure there is no memory leak, the *DotNetObjectReference* is disposed after the JS-call. 366 | That means, immediately after the JS-call the callback is no longer available. 367 | So, the JS-function must outlast the callback, otherwise a "*System.ArgumentException: There is no tracked object with id ...*" occurs. 368 | In sync-calls everything works fine, 369 | but when your callback is async, your JS function must also be async and must complete after the callback completes. 370 | 371 | ### Nested Functions or Returning a Function 372 | 373 | A callback can have its own parameters and return-type. 374 | If you put another callback as parameter or return-type, 375 | the generated type will be *CALLBACK_INSIDE_CALLBACK_NOT_SUPPORTED* or *CALLBACK_RETURN_TYPE_NOT_SUPPORTED*, what leads to a compile error. 376 | Only callbacks as parameters without nesting are supported. 377 | 378 | 379 |

380 | ## Release Notes 381 | 382 | - 0.0.1 383 | - first version, includes all basic functionalities for generating TSRuntime 384 | - 0.1 385 | - improved declaration path: Instead of one include string, an array of objects { "include": string, "excludes": string[], "file module path": string } is now supported 386 | - 0.2 387 | - optional parameters and default parameter values are now supported 388 | - 0.3 389 | - breaking changes: changed config keys, defaults and properties in Config, changed Config.FromJson(string json) to new Config(string json) 390 | - added key "generate on save" and "action name" keys to config 391 | - 0.4 392 | - module grouping is now supported 393 | - small breaking change: A namespace that contains IServiceCollection is required when serviceExtension is enabled and namespace *Microsoft.Extensions.DependencyInjection* was added to the defaults 394 | - 0.5 395 | - generics in type map are now supported 396 | - 0.6 397 | - \*\*\* huge Refactoring, many breaking changes \*\*\* 398 | - renamed the project, repository and NuGet package to "Blazor.TSRuntime" (before it was "TSRuntime") 399 | - dropped *Programmatically Usage* and *Visual Studio Extension*, only *Source Generator* will be continued -> reduced project structure to 2 projects 400 | - changed ISourceGenerator to IIncrementalGenerator 401 | - *tsconfig.tsruntime.json* can now be named *\*.tsruntime.json* 402 | - .d.ts-files must be added with *<AdditionalFiles Include="\*\*\\\*.d.ts" />* 403 | - added config key *webroot path* 404 | - moved config key *[module grouping].[service extension]* to *[service extension]* 405 | - renamed key "declaration path" to "input path" 406 | - renamed key "file module path" to "module path" 407 | - renamed key "append Async" to "append async" 408 | - Config.InputPath.ModulePath must end with ".js" 409 | - 0.7 410 | - breaking change: [input path] ('include', 'excludes', 'module path') must start with '/' 411 | - generic TS-functions are now supported 412 | - TS-function description is mapped to C# method description. Currently supported tags are <summary>, <remarks>, <param>, <returns> 413 | - JS-files with JSDocs type annotations are now supported 414 | - TS-files are now supported 415 | - 0.8 416 | - scripts are supported (non-module-files: js-files that are included via <script> tag) 417 |

418 | - 1.0 419 | - callbacks are supported: Mapping parameters of a function type to the corresponding C# delegate (*Action<>*/*Func<>*) 420 | - JSDoc "@typeparam" tag is now supported 421 | -------------------------------------------------------------------------------- /Blazor.TSRuntime.Tests/GeneratorTests/GenericsTests/GeneratorGenericsTests.JSGenerics.verified.txt: -------------------------------------------------------------------------------- 1 | --------- 2 | TSRuntime 3 | --------- 4 | 5 | // 6 | #pragma warning disable 7 | #nullable enable annotations 8 | 9 | 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace Microsoft.JSInterop; 16 | 17 | /// 18 | /// An implementation for . 19 | /// It manages JS-modules: It loads the modules, caches it in an array and disposing releases all modules. 20 | /// 21 | /// There is 1 module available: GenericModule 22 | /// 23 | /// 24 | [System.CodeDom.Compiler.GeneratedCodeAttribute("Blazor.TSRuntime", "X.X.X")] 25 | public sealed class TSRuntime(IJSRuntime jsRuntime) : ITSRuntime, IDisposable, IAsyncDisposable { 26 | private readonly CancellationTokenSource cancellationTokenSource = new(); 27 | 28 | Task ITSRuntime.GetGenericModuleModule() => GetGenericModuleModule(); 29 | private Task? GenericModuleModule; 30 | private Task GetGenericModuleModule() 31 | => GenericModuleModule switch { 32 | Task { IsCompletedSuccessfully: true } 33 | or Task { IsCompleted: false } => GenericModuleModule, 34 | _ => GenericModuleModule = jsRuntime.InvokeAsync("import", cancellationTokenSource.Token, "/GenericModule.js").AsTask() 35 | }; 36 | 37 | public Task PreloadAllModules() { 38 | GetGenericModuleModule(); 39 | 40 | return Task.WhenAll([GenericModuleModule!]); 41 | } 42 | 43 | 44 | TResult ITSRuntime.TSInvoke(string identifier, object?[]? args) => ((IJSInProcessRuntime)jsRuntime).Invoke(identifier, args); 45 | 46 | ValueTask ITSRuntime.TSInvokeTrySync(string identifier, object?[]? args, CancellationToken cancellationToken) { 47 | if (jsRuntime is IJSInProcessRuntime jsInProcessRuntime) 48 | return ValueTask.FromResult(jsInProcessRuntime.Invoke(identifier, args)); 49 | else 50 | return jsRuntime.InvokeAsync(identifier, cancellationToken, args); 51 | } 52 | 53 | ValueTask ITSRuntime.TSInvokeAsync(string identifier, object?[]? args, CancellationToken cancellationToken) 54 | => jsRuntime.InvokeAsync(identifier, cancellationToken, args); 55 | 56 | 57 | TResult ITSRuntime.TSInvoke(Task moduleTask, string identifier, object?[]? args) { 58 | if (!moduleTask.IsCompletedSuccessfully) 59 | throw new JSException("JS-module is not loaded. Use and await the Preload()-method to ensure the module is loaded."); 60 | 61 | return ((IJSInProcessObjectReference)moduleTask.Result).Invoke(identifier, args); 62 | } 63 | 64 | async ValueTask ITSRuntime.TSInvokeTrySync(Task moduleTask, string identifier, object?[]? args, CancellationToken cancellationToken) { 65 | IJSObjectReference module = await moduleTask; 66 | if (module is IJSInProcessObjectReference inProcessModule) 67 | return inProcessModule.Invoke(identifier, args); 68 | else 69 | return await module.InvokeAsync(identifier, cancellationToken, args); 70 | } 71 | 72 | async ValueTask ITSRuntime.TSInvokeAsync(Task moduleTask, string identifier, object?[]? args, CancellationToken cancellationToken) { 73 | IJSObjectReference module = await moduleTask; 74 | return await module.InvokeAsync(identifier, cancellationToken, args); 75 | } 76 | 77 | 78 | TResult ITSRuntime.TSInvoke(string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args) where TCallback : class => default; // no callbacks are used 79 | 80 | ValueTask ITSRuntime.TSInvokeTrySync(string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class => default; // no callbacks are used 81 | 82 | ValueTask ITSRuntime.TSInvokeAsync(string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class => default; // no callbacks are used 83 | 84 | 85 | TResult ITSRuntime.TSInvoke(Task moduleTask, string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args) where TCallback : class => default; // no callbacks are used 86 | 87 | ValueTask ITSRuntime.TSInvokeTrySync(Task moduleTask, string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class => default; // no callbacks are used 88 | 89 | ValueTask ITSRuntime.TSInvokeAsync(Task moduleTask, string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class => default; // no callbacks are used 90 | 91 | 92 | 93 | /// 94 | /// Releases each module synchronously if possible, otherwise asynchronously per fire and forget. 95 | /// 96 | public void Dispose() { 97 | if (cancellationTokenSource.IsCancellationRequested) 98 | return; 99 | 100 | cancellationTokenSource.Cancel(); 101 | cancellationTokenSource.Dispose(); 102 | 103 | if (GenericModuleModule?.IsCompletedSuccessfully == true) 104 | if (GenericModuleModule.Result is IJSInProcessObjectReference inProcessModule) 105 | inProcessModule.Dispose(); 106 | else 107 | _ = GenericModuleModule.Result.DisposeAsync().Preserve(); 108 | GenericModuleModule = null; 109 | } 110 | 111 | /// 112 | /// Releases each module synchronously if possible, otherwise asynchronously and returns a task that completes, when all module disposing tasks complete. 113 | /// The asynchronous disposing tasks are happening in parallel. 114 | /// 115 | /// 116 | public ValueTask DisposeAsync() { 117 | if (cancellationTokenSource.IsCancellationRequested) 118 | return ValueTask.CompletedTask; 119 | 120 | cancellationTokenSource.Cancel(); 121 | cancellationTokenSource.Dispose(); 122 | 123 | List taskList = new(1); 124 | 125 | if (GenericModuleModule?.IsCompletedSuccessfully == true) 126 | if (GenericModuleModule.Result is IJSInProcessObjectReference inProcessModule) 127 | inProcessModule.Dispose(); 128 | else { 129 | ValueTask valueTask = GenericModuleModule.Result.DisposeAsync(); 130 | if (!valueTask.IsCompleted) 131 | taskList.Add(valueTask.AsTask()); 132 | } 133 | GenericModuleModule = null; 134 | 135 | if (taskList.Count == 0) 136 | return ValueTask.CompletedTask; 137 | else 138 | return new ValueTask(Task.WhenAll(taskList)); 139 | } 140 | } 141 | 142 | 143 | ---------- 144 | ITSRuntime 145 | ---------- 146 | 147 | // 148 | #pragma warning disable 149 | #nullable enable annotations 150 | 151 | 152 | using System.Threading; 153 | using System.Threading.Tasks; 154 | 155 | namespace Microsoft.JSInterop; 156 | 157 | /// 158 | /// Interface for JS-interop. 159 | /// It contains an invoke-method for every js-function, a preload-method for every module and a method to load all modules. 160 | /// 161 | [System.CodeDom.Compiler.GeneratedCodeAttribute("Blazor.TSRuntime", "X.X.X")] 162 | public partial interface ITSRuntime { 163 | /// 164 | /// Fetches all modules as javascript-modules. 165 | /// If already loading, it doesn't trigger a second loading and if any already loaded, these are not loaded again, so if all already loaded, it returns a completed task. 166 | /// 167 | /// A Task that will complete when all module loading Tasks have completed. 168 | public Task PreloadAllModules(); 169 | 170 | 171 | 172 | /// 173 | /// Invokes the specified JavaScript function synchronously. 174 | /// If module is not loaded or synchronous is not supported, it fails with an exception. 175 | /// 176 | /// 177 | /// name of the javascript function 178 | /// parameter passing to the JS-function 179 | /// 180 | protected TResult TSInvoke(string identifier, object?[]? args); 181 | 182 | /// 183 | /// Invokes the specified JavaScript function synchronously when supported, otherwise asynchronously. 184 | /// 185 | /// 186 | /// name of the javascript function 187 | /// parameter passing to the JS-function 188 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 189 | /// 190 | protected ValueTask TSInvokeTrySync(string identifier, object?[]? args, CancellationToken cancellationToken); 191 | 192 | /// 193 | /// Invokes the specified JavaScript function asynchronously. 194 | /// 195 | /// 196 | /// name of the javascript function 197 | /// parameter passing to the JS-function 198 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 199 | /// 200 | protected ValueTask TSInvokeAsync(string identifier, object?[]? args, CancellationToken cancellationToken); 201 | 202 | 203 | /// 204 | /// Invokes the specified JavaScript function in the specified module synchronously. 205 | /// If module is not loaded or synchronous is not supported, it fails with an exception. 206 | /// 207 | /// 208 | /// The loading task of a module 209 | /// name of the javascript function 210 | /// parameter passing to the JS-function 211 | /// 212 | protected TResult TSInvoke(Task moduleTask, string identifier, object?[]? args); 213 | 214 | /// 215 | /// Invokes the specified JavaScript function in the specified module synchronously when supported, otherwise asynchronously. 216 | /// 217 | /// 218 | /// The loading task of a module 219 | /// name of the javascript function 220 | /// parameter passing to the JS-function 221 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 222 | /// 223 | protected ValueTask TSInvokeTrySync(Task moduleTask, string identifier, object?[]? args, CancellationToken cancellationToken); 224 | 225 | /// 226 | /// Invokes the specified JavaScript function in the specified module asynchronously. 227 | /// 228 | /// 229 | /// The loading task of a module 230 | /// name of the javascript function 231 | /// parameter passing to the JS-function 232 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 233 | /// 234 | protected ValueTask TSInvokeAsync(Task moduleTask, string identifier, object?[]? args, CancellationToken cancellationToken); 235 | 236 | 237 | /// 238 | /// Invokes the specified JavaScript function synchronously. 239 | /// If module is not loaded or synchronous is not supported, it fails with an exception. 240 | /// 241 | /// 242 | /// 243 | /// name of the javascript function 244 | /// reference to a csharp object with callback functions 245 | /// parameter passing to the JS-function 246 | /// 247 | protected TResult TSInvoke(string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args) where TCallback : class; 248 | 249 | /// 250 | /// Invokes the specified JavaScript function synchronously when supported, otherwise asynchronously. 251 | /// 252 | /// 253 | /// 254 | /// name of the javascript function 255 | /// reference to a csharp object with callback functions 256 | /// parameter passing to the JS-function 257 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 258 | /// 259 | protected ValueTask TSInvokeTrySync(string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class; 260 | 261 | /// 262 | /// Invokes the specified JavaScript function asynchronously. 263 | /// 264 | /// 265 | /// 266 | /// name of the javascript function 267 | /// reference to a csharp object with callback functions 268 | /// parameter passing to the JS-function 269 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 270 | /// 271 | protected ValueTask TSInvokeAsync(string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class; 272 | 273 | 274 | /// 275 | /// Invokes the specified JavaScript function in the specified module synchronously. 276 | /// If module is not loaded or synchronous is not supported, it fails with an exception. 277 | /// 278 | /// 279 | /// 280 | /// The loading task of a module 281 | /// name of the javascript function 282 | /// reference to a csharp object with callback functions 283 | /// parameter passing to the JS-function 284 | /// 285 | protected TResult TSInvoke(Task moduleTask, string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args) where TCallback : class; 286 | 287 | /// 288 | /// Invokes the specified JavaScript function in the specified module synchronously when supported, otherwise asynchronously. 289 | /// 290 | /// 291 | /// 292 | /// The loading task of a module 293 | /// name of the javascript function 294 | /// reference to a csharp object with callback functions 295 | /// parameter passing to the JS-function 296 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 297 | /// 298 | protected ValueTask TSInvokeTrySync(Task moduleTask, string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class; 299 | 300 | /// 301 | /// Invokes the specified JavaScript function in the specified module asynchronously. 302 | /// 303 | /// 304 | /// 305 | /// The loading task of a module 306 | /// name of the javascript function 307 | /// reference to a csharp object with callback functions 308 | /// parameter passing to the JS-function 309 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 310 | /// 311 | protected ValueTask TSInvokeAsync(Task moduleTask, string identifier, DotNetObjectReference dotNetObjectReference, object?[]? args, CancellationToken cancellationToken) where TCallback : class; 312 | } 313 | 314 | 315 | ------ 316 | Module 317 | ------ 318 | 319 | // 320 | #pragma warning disable 321 | #nullable enable annotations 322 | 323 | 324 | using System.Threading; 325 | using System.Threading.Tasks; 326 | using Microsoft.AspNetCore.Components; 327 | using System.Numerics; 328 | 329 | namespace Microsoft.JSInterop; 330 | 331 | public partial interface ITSRuntime { 332 | protected Task GetGenericModuleModule(); 333 | 334 | /// 335 | /// Loads 'GenericModule' (/GenericModule.js) as javascript-module. 336 | /// If already loading, it does not trigger a second loading and if already loaded, it returns a completed task. 337 | /// 338 | /// A Task that will complete when the module import have completed. 339 | public Task PreloadGenericModule() => GetGenericModuleModule(); 340 | 341 | 342 | /// 343 | /// Invokes in module 'GenericModule' the JS-function 'generic' synchronously when supported, otherwise asynchronously. 344 | /// 345 | /// 346 | /// 347 | /// 348 | /// A cancellation token to signal the cancellation of the operation. Specifying this parameter will override any default cancellations such as due to timeouts () from being applied. 349 | /// Result of the JS-function. 350 | public async ValueTask Generic(CancellationToken cancellationToken = default) { 351 | return await TSInvokeTrySync(GetGenericModuleModule(), "generic", [], cancellationToken); 352 | } 353 | } 354 | --------------------------------------------------------------------------------