├── 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 | 
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