From 972658f42ad38cc6a655e1c9c0fea6e96bb28364 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 7 Apr 2022 11:02:36 -0500 Subject: [PATCH] Added support for custom type declaration sources. --- .../ILocalStorageService.cs | 7 +- .../Extensions/AttributeSyntaxExtensions.cs | 6 +- .../JavaScriptInteropGenerator.cs | 44 ++++---- .../Options/GeneratorOptions.cs | 31 +++++- ...cs => TypeDeclarationParser.Interfaces.cs} | 2 +- ...bDomParser.cs => TypeDeclarationParser.cs} | 12 ++- .../Readers/LibDomReader.cs | 98 ----------------- .../Readers/TypeDeclarationReader.Factory.cs | 35 ++++++ .../TypeDeclarationReader.LocalFiles.cs | 9 ++ .../TypeDeclarationReader.RemoteFile.cs | 22 ++++ .../Readers/TypeDeclarationReader.cs | 102 ++++++++++++++++++ .../SourceCode.JSAutoInteropAttribute.cs | 5 + .../LibDomParserInterfacesTests.cs | 4 +- .../LibDomParserTests.cs | 2 +- .../LibDomReaderTests.cs | 6 +- 15 files changed, 252 insertions(+), 133 deletions(-) rename src/Blazor.SourceGenerators/Parsers/{LibDomParser.Interfaces.cs => TypeDeclarationParser.Interfaces.cs} (96%) rename src/Blazor.SourceGenerators/Parsers/{LibDomParser.cs => TypeDeclarationParser.cs} (70%) delete mode 100644 src/Blazor.SourceGenerators/Readers/LibDomReader.cs create mode 100644 src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.Factory.cs create mode 100644 src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.LocalFiles.cs create mode 100644 src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.RemoteFile.cs create mode 100644 src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.cs diff --git a/src/Blazor.LocalStorage/ILocalStorageService.cs b/src/Blazor.LocalStorage/ILocalStorageService.cs index 116a897..9eb7855 100644 --- a/src/Blazor.LocalStorage/ILocalStorageService.cs +++ b/src/Blazor.LocalStorage/ILocalStorageService.cs @@ -3,13 +3,16 @@ namespace Microsoft.JSInterop; -/// [JSAutoInterop( TypeName = "Storage", Implementation = "window.localStorage", HostingModel = BlazorHostingModel.Server, OnlyGeneratePureJS = true, - Url = "https://developer.mozilla.org/docs/Web/API/Window/localStorage")] + Url = "https://developer.mozilla.org/docs/Web/API/Window/localStorage", + TypeDeclarationSources = new[] + { + "https://raw.githubusercontent.com/microsoft/TypeScript/main/lib/lib.dom.d.ts" + })] public partial interface ILocalStorageService { } \ No newline at end of file diff --git a/src/Blazor.SourceGenerators/Extensions/AttributeSyntaxExtensions.cs b/src/Blazor.SourceGenerators/Extensions/AttributeSyntaxExtensions.cs index 04ee5b1..ba562d3 100644 --- a/src/Blazor.SourceGenerators/Extensions/AttributeSyntaxExtensions.cs +++ b/src/Blazor.SourceGenerators/Extensions/AttributeSyntaxExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) David Pine. All rights reserved. // Licensed under the MIT License. - - namespace Blazor.SourceGenerators.Extensions; static class AttributeSyntaxExtensions @@ -49,6 +47,10 @@ internal static GeneratorOptions GetGeneratorOptions( { PureJavaScriptOverrides = ParseArray(arg.Expression.ToString()) }, + nameof(options.TypeDeclarationSources) => options with + { + TypeDeclarationSources = ParseArray(arg.Expression.ToString()) + }, _ => options }; diff --git a/src/Blazor.SourceGenerators/JavaScriptInteropGenerator.cs b/src/Blazor.SourceGenerators/JavaScriptInteropGenerator.cs index 6b6cd1a..2dbda3f 100644 --- a/src/Blazor.SourceGenerators/JavaScriptInteropGenerator.cs +++ b/src/Blazor.SourceGenerators/JavaScriptInteropGenerator.cs @@ -6,7 +6,6 @@ namespace Blazor.SourceGenerators; [Generator] internal sealed partial class JavaScriptInteropGenerator : ISourceGenerator { - private readonly LibDomParser _libDomParser = new(); private readonly HashSet<(string FileName, string SourceCode)> _sourceCodeToAdd = new() { (nameof(RecordCompat).ToGeneratedFileName(), RecordCompat), @@ -63,27 +62,30 @@ public void Execute(GeneratorExecutionContext context) continue; } - var result = _libDomParser.ParseTargetType(options.TypeName!); - if (result.Status == ParserResultStatus.SuccessfullyParsed && - result.Value is not null) + foreach (var parser in options.Parsers) { - var namespaceString = - (typeSymbol.ContainingNamespace.ToDisplayString(), classDeclaration.Parent) switch - { - (string { Length: > 0 } containingNamespace, _) => containingNamespace, - (_, BaseNamespaceDeclarationSyntax namespaceDeclaration) => namespaceDeclaration.Name.ToString(), - _ => null - }; - var @interface = - options.Implementation.ToInterfaceName(); - var implementation = - options.Implementation.ToImplementationName(); - - var topLevelObject = result.Value; - context.AddDependentTypesSource(topLevelObject) - .AddInterfaceSource(topLevelObject, @interface, options, namespaceString) - .AddImplementationSource(topLevelObject, implementation, options, namespaceString) - .AddDependencyInjectionExtensionsSource(topLevelObject, implementation, options); + var result = parser.ParseTargetType(options.TypeName!); + if (result.Status == ParserResultStatus.SuccessfullyParsed && + result.Value is not null) + { + var namespaceString = + (typeSymbol.ContainingNamespace.ToDisplayString(), classDeclaration.Parent) switch + { + (string { Length: > 0 } containingNamespace, _) => containingNamespace, + (_, BaseNamespaceDeclarationSyntax namespaceDeclaration) => namespaceDeclaration.Name.ToString(), + _ => null + }; + var @interface = + options.Implementation.ToInterfaceName(); + var implementation = + options.Implementation.ToImplementationName(); + + var topLevelObject = result.Value; + context.AddDependentTypesSource(topLevelObject) + .AddInterfaceSource(topLevelObject, @interface, options, namespaceString) + .AddImplementationSource(topLevelObject, implementation, options, namespaceString) + .AddDependencyInjectionExtensionsSource(topLevelObject, implementation, options); + } } } } diff --git a/src/Blazor.SourceGenerators/Options/GeneratorOptions.cs b/src/Blazor.SourceGenerators/Options/GeneratorOptions.cs index c6aa147..3bd36bb 100644 --- a/src/Blazor.SourceGenerators/Options/GeneratorOptions.cs +++ b/src/Blazor.SourceGenerators/Options/GeneratorOptions.cs @@ -11,6 +11,7 @@ /// The optional URL to the corresponding API. /// The optional generic method descriptors value from the parsed JSAutoGenericInteropAttribute.GenericMethodDescriptors. /// Overrides pure JavaScript calls. A custom impl must exist. +/// An optional array of TypeScript type declarations sources. Valid values are URLs or file paths. /// /// A value indicating whether to generate targeting WASM: /// When true: Synchronous extensions are generated on the IJSInProcessRuntime type. @@ -24,4 +25,32 @@ internal sealed record GeneratorOptions( string? Url = null, string[]? GenericMethodDescriptors = null, string[]? PureJavaScriptOverrides = null, - bool IsWebAssembly = true); + string[]? TypeDeclarationSources = null, + bool IsWebAssembly = true) +{ + ISet? _parsers; + + /// + /// Get instance maps its + /// into a set of parsers. + /// When is null, or empty, + /// the default lib.dom.d.ts parser is used. + /// + internal ISet Parsers + { + get + { + _parsers ??= new HashSet(); + + foreach (var source in + TypeDeclarationSources?.Select(TypeDeclarationReader.Factory) + ?.Select(reader => new TypeDeclarationParser(reader)) + ?? new[] { TypeDeclarationParser.Default }) + { + _parsers.Add(source); + } + + return _parsers; + } + } +} diff --git a/src/Blazor.SourceGenerators/Parsers/LibDomParser.Interfaces.cs b/src/Blazor.SourceGenerators/Parsers/TypeDeclarationParser.Interfaces.cs similarity index 96% rename from src/Blazor.SourceGenerators/Parsers/LibDomParser.Interfaces.cs rename to src/Blazor.SourceGenerators/Parsers/TypeDeclarationParser.Interfaces.cs index 88686ca..3b4a768 100644 --- a/src/Blazor.SourceGenerators/Parsers/LibDomParser.Interfaces.cs +++ b/src/Blazor.SourceGenerators/Parsers/TypeDeclarationParser.Interfaces.cs @@ -3,7 +3,7 @@ namespace Blazor.SourceGenerators.Parsers; -internal sealed partial class LibDomParser +internal sealed partial class TypeDeclarationParser { internal CSharpObject? ToObject(string typeScriptTypeDeclaration) { diff --git a/src/Blazor.SourceGenerators/Parsers/LibDomParser.cs b/src/Blazor.SourceGenerators/Parsers/TypeDeclarationParser.cs similarity index 70% rename from src/Blazor.SourceGenerators/Parsers/LibDomParser.cs rename to src/Blazor.SourceGenerators/Parsers/TypeDeclarationParser.cs index 444a01d..ef0f7fa 100644 --- a/src/Blazor.SourceGenerators/Parsers/LibDomParser.cs +++ b/src/Blazor.SourceGenerators/Parsers/TypeDeclarationParser.cs @@ -3,9 +3,17 @@ namespace Blazor.SourceGenerators.Parsers; -internal sealed partial class LibDomParser +internal sealed partial class TypeDeclarationParser { - private readonly LibDomReader _reader = new(); + static readonly Lazy s_defaultParser = + new( + valueFactory: () => new TypeDeclarationParser(TypeDeclarationReader.Default)); + + readonly TypeDeclarationReader _reader; + + internal static TypeDeclarationParser Default => s_defaultParser.Value; + + internal TypeDeclarationParser(TypeDeclarationReader reader) => _reader = reader; public ParserResult ParseTargetType(string typeName) { diff --git a/src/Blazor.SourceGenerators/Readers/LibDomReader.cs b/src/Blazor.SourceGenerators/Readers/LibDomReader.cs deleted file mode 100644 index 1c5c964..0000000 --- a/src/Blazor.SourceGenerators/Readers/LibDomReader.cs +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) David Pine. All rights reserved. -// Licensed under the MIT License. - -namespace Blazor.SourceGenerators.Readers; - -internal sealed class LibDomReader -{ - private static readonly HttpClient _httpClient = new(); - - private static readonly Lazy _libDomText = new(() => - { - var rawUrl = - "https://raw.githubusercontent.com/microsoft/TypeScript/main/lib/lib.dom.d.ts"; - var libDomDefinitionTypeScript = - _httpClient.GetStringAsync(rawUrl) - .ConfigureAwait(false) - .GetAwaiter() - .GetResult(); - - return libDomDefinitionTypeScript; - }); - - private readonly Lazy> _lazyTypeDeclarationMap = - new(() => - { - ConcurrentDictionary map = new(); - - try - { - var libDomDefinitionTypeScript = _libDomText.Value; - if (libDomDefinitionTypeScript is { Length: > 0 }) - { - var matchCollection = InterfaceRegex.Matches(libDomDefinitionTypeScript).Cast().Select(m => m.Value); - Parallel.ForEach( - matchCollection, - match => - { - var typeName = InterfaceTypeNameRegex.GetMatchGroupValue(match, "TypeName"); - if (typeName is not null) - { - map[typeName] = match; - } - }); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error intializing lib dom parser. {ex}"); - } - - return map; - }); - - private readonly Lazy> _lazyTypeAliasMap = - new(() => - { - ConcurrentDictionary map = new(); - - try - { - var libDomDefinitionTypeScript = _libDomText.Value; - if (libDomDefinitionTypeScript is { Length: > 0 }) - { - var matchCollection = TypeRegex.Matches(libDomDefinitionTypeScript).Cast().Select(m => m.Value); - Parallel.ForEach( - matchCollection, - match => - { - var typeName = TypeNameRegex.GetMatchGroupValue(match, "TypeName"); - if (typeName is not null) - { - map[typeName] = match; - } - }); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error intializing lib dom parser. {ex}"); - } - - return map; - }); - - - /// - /// For testing purposes. - /// - internal bool IsInitialized => _lazyTypeDeclarationMap is { IsValueCreated: true }; - - public bool TryGetDeclaration( - string typeName, out string? declaration) => - _lazyTypeDeclarationMap.Value.TryGetValue(typeName, out declaration); - - public bool TryGetTypeAlias( - string typeAliasName, out string? typeAlias) => - _lazyTypeAliasMap.Value.TryGetValue(typeAliasName, out typeAlias); -} diff --git a/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.Factory.cs b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.Factory.cs new file mode 100644 index 0000000..de77f07 --- /dev/null +++ b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.Factory.cs @@ -0,0 +1,35 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace Blazor.SourceGenerators.Readers; + +internal sealed partial class TypeDeclarationReader +{ + static readonly ConcurrentDictionary s_readerCache = + new(StringComparer.OrdinalIgnoreCase); + + internal static TypeDeclarationReader Factory(string source) + { + var uri = new Uri(source); + var sourceKey = uri.IsFile ? uri.LocalPath : uri.OriginalString; + + var reader = + s_readerCache.GetOrAdd( + sourceKey, _ => new TypeDeclarationReader(uri)); + + return reader; + } + + internal static TypeDeclarationReader Default + { + get + { + var sourceKey = s_defaultTypeDeclarationSource.OriginalString; + var reader = + s_readerCache.GetOrAdd( + sourceKey, _ => new TypeDeclarationReader(s_defaultTypeDeclarationSource)); + + return reader; + } + } +} diff --git a/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.LocalFiles.cs b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.LocalFiles.cs new file mode 100644 index 0000000..688e979 --- /dev/null +++ b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.LocalFiles.cs @@ -0,0 +1,9 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace Blazor.SourceGenerators.Readers; + +internal sealed partial class TypeDeclarationReader +{ + string GetLocalFileText(string filePath) => File.ReadAllText(filePath); +} diff --git a/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.RemoteFile.cs b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.RemoteFile.cs new file mode 100644 index 0000000..910bc94 --- /dev/null +++ b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.RemoteFile.cs @@ -0,0 +1,22 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace Blazor.SourceGenerators.Readers; + +internal sealed partial class TypeDeclarationReader +{ + static readonly HttpClient s_httpClient = new(); + static readonly Uri s_defaultTypeDeclarationSource = + new("https://raw.githubusercontent.com/microsoft/TypeScript/main/lib/lib.dom.d.ts"); + + string GetRemoteFileText(string url) + { + var typeDeclarationText = + s_httpClient.GetStringAsync(url) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + + return typeDeclarationText; + } +} diff --git a/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.cs b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.cs new file mode 100644 index 0000000..8f59aa7 --- /dev/null +++ b/src/Blazor.SourceGenerators/Readers/TypeDeclarationReader.cs @@ -0,0 +1,102 @@ +// Copyright (c) David Pine. All rights reserved. +// Licensed under the MIT License. + +namespace Blazor.SourceGenerators.Readers; + +internal sealed partial class TypeDeclarationReader +{ + readonly Uri _typeDeclarationSource; + readonly Lazy _typeDeclarationText; + + IDictionary? _typeDeclarationMap; + IDictionary? _typeAliasMap; + + private IDictionary TypeDeclarationMap => + _typeDeclarationMap ??= ReadTypeDeclarationMap(_typeDeclarationText.Value); + + private IDictionary TypeAliasMap => + _typeAliasMap ??= ReadTypeAliasMap(_typeDeclarationText.Value); + + private TypeDeclarationReader( + Uri? typeDeclarationSource = null) + { + _typeDeclarationSource = typeDeclarationSource ?? s_defaultTypeDeclarationSource; + _typeDeclarationText = new Lazy( + valueFactory: () => _typeDeclarationSource.IsFile + ? GetLocalFileText(_typeDeclarationSource.LocalPath) + : GetRemoteFileText(_typeDeclarationSource.OriginalString)); + } + + IDictionary ReadTypeDeclarationMap(string typeDeclarations) + { + ConcurrentDictionary map = new(); + + try + { + if (typeDeclarations is { Length: > 0 }) + { + var matchCollection = + InterfaceRegex.Matches(typeDeclarations).Cast().Select(m => m.Value); + Parallel.ForEach( + matchCollection, + match => + { + var typeName = InterfaceTypeNameRegex.GetMatchGroupValue(match, "TypeName"); + if (typeName is not null) + { + map[typeName] = match; + } + }); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error intializing lib dom parser. {ex}"); + } + + return map; + } + + IDictionary ReadTypeAliasMap(string typeDeclarations) + { + ConcurrentDictionary map = new(); + + try + { + if (typeDeclarations is { Length: > 0 }) + { + var matchCollection = + TypeRegex.Matches(typeDeclarations).Cast().Select(m => m.Value); + Parallel.ForEach( + matchCollection, + match => + { + var typeName = TypeNameRegex.GetMatchGroupValue(match, "TypeName"); + if (typeName is not null) + { + map[typeName] = match; + } + }); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error intializing lib dom parser. {ex}"); + } + + return map; + } + + /// + /// For testing purposes. + /// + internal bool IsInitialized => TypeDeclarationMap is { Count: > 0 }; + + public bool TryGetDeclaration( + string typeName, out string? declaration) => + TypeDeclarationMap.TryGetValue(typeName, out declaration); + + public bool TryGetTypeAlias( + string typeAliasName, out string? typeAlias) => + TypeAliasMap.TryGetValue(typeAliasName, out typeAlias); +} diff --git a/src/Blazor.SourceGenerators/Source/SourceCode.JSAutoInteropAttribute.cs b/src/Blazor.SourceGenerators/Source/SourceCode.JSAutoInteropAttribute.cs index b0ccbf0..e6475cf 100644 --- a/src/Blazor.SourceGenerators/Source/SourceCode.JSAutoInteropAttribute.cs +++ b/src/Blazor.SourceGenerators/Source/SourceCode.JSAutoInteropAttribute.cs @@ -58,6 +58,11 @@ public class JSAutoInteropAttribute : Attribute /// The optional URL to the corresponding API. /// public string? Url { get; set; } + + /// + /// An optional array of TypeScript type declarations sources. Valid values are URLs or file paths. + /// + public string[]? TypeDeclarationSources { get; set; } } "; } \ No newline at end of file diff --git a/tests/Blazor.SourceGenerators.Tests/LibDomParserInterfacesTests.cs b/tests/Blazor.SourceGenerators.Tests/LibDomParserInterfacesTests.cs index 4c3f9cb..bd79a34 100644 --- a/tests/Blazor.SourceGenerators.Tests/LibDomParserInterfacesTests.cs +++ b/tests/Blazor.SourceGenerators.Tests/LibDomParserInterfacesTests.cs @@ -20,7 +20,7 @@ public void CorrectlyConvertsTypeScriptInterfaceToCSharpClass() sessionTypes?: string[]; videoCapabilities?: MediaKeySystemMediaCapability[]; }"; - var sut = new LibDomParser(); + var sut = TypeDeclarationParser.Default; var actual = sut.ToObject(text); var expected = @"#nullable enable using System.Text.Json.Serialization; @@ -88,7 +88,7 @@ public void CorrectlyConvertsTypeScriptInterfaceToCSharpExtensionObject() getCurrentPosition(successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions): void; watchPosition(successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions): number; }"; - var sut = new LibDomParser(); + var sut = TypeDeclarationParser.Default; var actual = sut.ToTopLevelObject(text); Assert.NotNull(actual); diff --git a/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs b/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs index 3311f4f..fe57a9f 100644 --- a/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs +++ b/tests/Blazor.SourceGenerators.Tests/LibDomParserTests.cs @@ -11,7 +11,7 @@ public class LibDomParserTests [Fact] public void ParseStaticObjectCorrectly() { - var sut = new LibDomParser(); + var sut = TypeDeclarationParser.Default; var parserResult = sut.ParseTargetType("Geolocation"); Assert.Equal(ParserResultStatus.SuccessfullyParsed, parserResult.Status); diff --git a/tests/Blazor.SourceGenerators.Tests/LibDomReaderTests.cs b/tests/Blazor.SourceGenerators.Tests/LibDomReaderTests.cs index 9b3b2d4..58f1a6d 100644 --- a/tests/Blazor.SourceGenerators.Tests/LibDomReaderTests.cs +++ b/tests/Blazor.SourceGenerators.Tests/LibDomReaderTests.cs @@ -14,7 +14,7 @@ public void InitializesTypeDefinitionsCorrectly() { var stopwatch = Stopwatch.StartNew(); - var sut = new LibDomReader(); + var sut = TypeDeclarationReader.Default; _ = sut.TryGetDeclaration("foo", out var _); stopwatch.Stop(); @@ -53,7 +53,7 @@ public static IEnumerable TryGetDeclarationInput ] public void TryGetDeclarationReturnsCorrectly(string typeName, string expected) { - var sut = new LibDomReader(); + var sut = TypeDeclarationReader.Default; var result = sut.TryGetDeclaration(typeName, out var actual); Assert.True(result); @@ -79,7 +79,7 @@ public static IEnumerable TryGetTypeAliasInput ] public void TryGetTypeAliasReturnsCorrectly(string typeAlias, string expected) { - var sut = new LibDomReader(); + var sut = TypeDeclarationReader.Default; var result = sut.TryGetTypeAlias(typeAlias, out var actual); Assert.True(result);