From bb4ee5c62c761f15c1885a85fbe2173104129053 Mon Sep 17 00:00:00 2001 From: s97712 Date: Sun, 14 Sep 2025 16:07:55 +0800 Subject: [PATCH] feat: Add suffix parameter support to ResolveNS method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core Changes: - Refactor INSResolver interface: ResolveNS(Type, string suffix) - Update DefaultNSResolver: support custom suffix path generation - Modify ScriptBridge: add suffix parameter passing - Update code generator: Razor(.razor) and Entry(.entry) suffix support - Fix dependency injection: delegate type Func - Complete test coverage: all 32 unit tests passing - Update project documentation: add suffix parameter usage guide Path Generation Rules: - Component.razor.ts → /js/Component.razor.js - Module.entry.ts → /js/Module.entry.js - Utils.ts → /js/Utils.js Breaking Change: ResolveNS method signature changed, all call sites updated --- .../INSResolverTests.cs | 98 ++++++++++++++++--- BlazorTS.SourceGenerator/ResolveGenerator.cs | 98 +++++++++---------- BlazorTS/INSResolver.cs | 34 ++++--- BlazorTS/ScriptBridge.cs | 5 +- BlazorTS/ServiceCollectionExtensions.cs | 4 +- README.md | 27 ++++- README_CN.md | 27 ++++- docs/development-guide.md | 17 +++- 8 files changed, 223 insertions(+), 87 deletions(-) diff --git a/BlazorTS.SourceGenerator.Tests/INSResolverTests.cs b/BlazorTS.SourceGenerator.Tests/INSResolverTests.cs index 1e545b7..db6990a 100644 --- a/BlazorTS.SourceGenerator.Tests/INSResolverTests.cs +++ b/BlazorTS.SourceGenerator.Tests/INSResolverTests.cs @@ -14,7 +14,7 @@ public void DefaultNSResolver_WithDefaultConstructor_UsesDefaultRule() var testType = typeof(TestClass); // Act - var result = resolver.ResolveNS(testType); + var result = resolver.ResolveNS(testType, ""); // Assert Assert.NotNull(result); @@ -27,21 +27,30 @@ public void DefaultNSResolver_WithDefaultConstructor_UsesDefaultRule() public void DefaultNSResolver_WithCustomFunction_UsesCustomRule() { // Arrange - var customResolver = new DefaultNSResolver(type => $"/custom/{type.Name}.module.js"); + var customResolver = new DefaultNSResolver((type, suffix) => $"/custom/{type.Name}{suffix}.module.js"); var testType = typeof(TestClass); // Act - var result = customResolver.ResolveNS(testType); + var result = customResolver.ResolveNS(testType, ".test"); // Assert - Assert.Equal("/custom/TestClass.module.js", result); + Assert.Equal("/custom/TestClass.test.module.js", result); } [Fact] - public void DefaultNSResolver_WithNullFunction_ThrowsArgumentNullException() + public void DefaultNSResolver_WithNullFunction_UsesDefaultRule() { - // Arrange & Act & Assert - Assert.Throws(() => new DefaultNSResolver(null!)); + // Arrange + var resolver = new DefaultNSResolver(null); + var testType = typeof(TestClass); + + // Act + var result = resolver.ResolveNS(testType, ""); + + // Assert + Assert.NotNull(result); + Assert.StartsWith("/js/", result); + Assert.EndsWith(".js", result); } [Fact] @@ -52,12 +61,65 @@ public void DefaultNSResolver_WithNestedClass_HandlesCorrectly() var nestedType = typeof(TestClass.NestedClass); // Act - var result = resolver.ResolveNS(nestedType); + var result = resolver.ResolveNS(nestedType, ""); + + // Assert + Assert.NotNull(result); + Assert.StartsWith("/js/", result); + Assert.EndsWith(".js", result); + } + + [Fact] + public void DefaultNSResolver_WithRazorSuffix_GeneratesRazorPath() + { + // Arrange + var resolver = new DefaultNSResolver(); + var testType = typeof(TestClass); + + // Act + var result = resolver.ResolveNS(testType, ".razor"); + + // Assert + Assert.NotNull(result); + Assert.StartsWith("/js/", result); + Assert.EndsWith(".razor.js", result); + Assert.Contains("TestClass", result); + } + + [Fact] + public void DefaultNSResolver_WithEntrySuffix_GeneratesEntryPath() + { + // Arrange + var resolver = new DefaultNSResolver(); + var testType = typeof(TestClass); + + // Act + var result = resolver.ResolveNS(testType, ".entry"); + + // Assert + Assert.NotNull(result); + Assert.StartsWith("/js/", result); + Assert.EndsWith(".entry.js", result); + Assert.Contains("TestClass", result); + } + + [Fact] + public void DefaultNSResolver_WithEmptySuffix_GeneratesBasicPath() + { + // Arrange + var resolver = new DefaultNSResolver(); + var testType = typeof(TestClass); + + // Act + var result = resolver.ResolveNS(testType, ""); // Assert Assert.NotNull(result); Assert.StartsWith("/js/", result); Assert.EndsWith(".js", result); + Assert.Contains("TestClass", result); + Assert.DoesNotContain(".razor", result); + Assert.DoesNotContain(".entry", result); } [Fact] @@ -68,12 +130,26 @@ public void CustomNSResolver_ImplementsInterface_WorksCorrectly() var testType = typeof(TestClass); // Act - var result = customResolver.ResolveNS(testType); + var result = customResolver.ResolveNS(testType, ""); // Assert Assert.Equal("/test/custom/TestClass.js", result); } + [Fact] + public void CustomNSResolver_WithSuffix_IncludesSuffixInPath() + { + // Arrange + var customResolver = new CustomTestResolver(); + var testType = typeof(TestClass); + + // Act + var result = customResolver.ResolveNS(testType, ".razor"); + + // Assert + Assert.Equal("/test/custom/TestClass.razor.js", result); + } + private class TestClass { public class NestedClass { } @@ -81,9 +157,9 @@ public class NestedClass { } private class CustomTestResolver : INSResolver { - public string ResolveNS(Type tsType) + public string ResolveNS(Type tsType, string suffix) { - return $"/test/custom/{tsType.Name}.js"; + return $"/test/custom/{tsType.Name}{suffix}.js"; } } } \ No newline at end of file diff --git a/BlazorTS.SourceGenerator/ResolveGenerator.cs b/BlazorTS.SourceGenerator/ResolveGenerator.cs index 29d85fb..2460b5d 100755 --- a/BlazorTS.SourceGenerator/ResolveGenerator.cs +++ b/BlazorTS.SourceGenerator/ResolveGenerator.cs @@ -131,28 +131,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context) Helper.Log(spc, $"Processing {files.Length} BlazorTS files with nativePath: {nativePath}"); - var entryModuleNames = new List(); - var razorInteropTypeNames = new List(); - - foreach (var file in files) - { - var fullName = Generate(spc, file.ClassName, file.NamespaceName, file.IsRazorComponent, file.Content); - if (!file.IsRazorComponent) - { - entryModuleNames.Add(fullName); - } - else - { - // Register the nested TSInterop for Razor components as injectable services - razorInteropTypeNames.Add($"{fullName}.TSInterop"); - } - } - - // Always generate the DI extension, even if there are no TS files. - var extensionCode = GenerateExtension(spc, entryModuleNames, razorInteropTypeNames); - var extensionFileName = "BlazorTS.SourceGenerator.Extensions.ServiceCollectionExtensions.g.cs"; - spc.AddSource(extensionFileName, SourceText.From(extensionCode, Encoding.UTF8)); - Helper.Log(spc, $"Generated service collection extension for {entryModuleNames.Count} entry modules and {razorInteropTypeNames.Count} razor interop types."); + var entryModuleNames = new List(); + var razorInteropTypeNames = new List(); + + foreach (var file in files) + { + var fullName = Generate(spc, file.ClassName, file.NamespaceName, file.IsRazorComponent, file.Content); + if (!file.IsRazorComponent) + { + entryModuleNames.Add(fullName); + } + else + { + // Register the nested TSInterop for Razor components as injectable services + razorInteropTypeNames.Add($"{fullName}.TSInterop"); + } + } + + // Always generate the DI extension, even if there are no TS files. + var extensionCode = GenerateExtension(spc, entryModuleNames, razorInteropTypeNames); + var extensionFileName = "BlazorTS.SourceGenerator.Extensions.ServiceCollectionExtensions.g.cs"; + spc.AddSource(extensionFileName, SourceText.From(extensionCode, Encoding.UTF8)); + Helper.Log(spc, $"Generated service collection extension for {entryModuleNames.Count} entry modules and {razorInteropTypeNames.Count} razor interop types."); }); @@ -225,7 +225,7 @@ public partial class {className} /// public class TSInterop(ScriptBridge invoker) {{ - private readonly string url = invoker.ResolveNS(typeof({fullName})); + private readonly string url = invoker.ResolveNS(typeof({fullName}), "".razor""); {methods.Select(GenerateMethod).ToDelimitedString("\n")} }} @@ -247,7 +247,7 @@ namespace {ns}; /// public class {className}(ScriptBridge invoker) {{ - private readonly string url = invoker.ResolveNS(typeof({fullName})); + private readonly string url = invoker.ResolveNS(typeof({fullName}), "".entry""); {methods.Select(GenerateMethod).ToDelimitedString("\n")} }} @@ -306,18 +306,18 @@ private static string ConvertType(string tsType) }; } - private static string GenerateExtension(SourceProductionContext spc, List entryModuleNames, List razorInteropTypeNames) - { - var servicesRegistration = entryModuleNames - .Select(name => $" services.AddScoped<{name}>();") - .Concat(razorInteropTypeNames.Select(name => $" services.AddScoped<{name}>();")) - .ToDelimitedString("\n"); - - return $@"#nullable enable -using Microsoft.Extensions.DependencyInjection; - -namespace BlazorTS.SourceGenerator.Extensions -{{ + private static string GenerateExtension(SourceProductionContext spc, List entryModuleNames, List razorInteropTypeNames) + { + var servicesRegistration = entryModuleNames + .Select(name => $" services.AddScoped<{name}>();") + .Concat(razorInteropTypeNames.Select(name => $" services.AddScoped<{name}>();")) + .ToDelimitedString("\n"); + + return $@"#nullable enable +using Microsoft.Extensions.DependencyInjection; + +namespace BlazorTS.SourceGenerator.Extensions +{{ /// /// Provides extension methods for setting up BlazorTS-generated script interop services. /// @@ -328,20 +328,20 @@ public static class ServiceCollectionExtensions /// /// The to add the services to. /// The so that additional calls can be chained. - public static IServiceCollection AddBlazorTSScripts(this IServiceCollection services) - {{ - // Ensure base BlazorTS services (ScriptBridge, INSResolver) are available - services.AddBlazorTS(); -{servicesRegistration} - return services; - }} - }} -}} -#nullable restore -"; - } + public static IServiceCollection AddBlazorTSScripts(this IServiceCollection services) + {{ + // Ensure base BlazorTS services (ScriptBridge, INSResolver) are available + services.AddBlazorTS(); +{servicesRegistration} + return services; + }} + }} +}} +#nullable restore +"; + } } -} +} diff --git a/BlazorTS/INSResolver.cs b/BlazorTS/INSResolver.cs index a1b5749..b68ad38 100644 --- a/BlazorTS/INSResolver.cs +++ b/BlazorTS/INSResolver.cs @@ -12,8 +12,9 @@ public interface INSResolver /// 解析 TypeScript 类型对应的 JavaScript 模块路径 /// /// TypeScript 类型 + /// 文件后缀,如 ".razor" 或 "" /// JavaScript 模块的 URL 路径 - string ResolveNS(Type tsType); + string ResolveNS(Type tsType, string suffix); } /// @@ -21,28 +22,33 @@ public interface INSResolver /// public sealed class DefaultNSResolver : INSResolver { - private readonly Func _resolveNS; + private readonly Func? _customResolver; /// - /// 使用默认规则创建解析器:移除程序集名前缀,映射到 /js/Namespace/Foo.js + /// 使用默认规则创建解析器 /// - public DefaultNSResolver() : this(static t => - { - var pkgName = Assembly.GetEntryAssembly()?.GetName().Name!; - var name = Regex.Replace(t.FullName!, $"^{pkgName}.", "").Replace(".", "/"); - return $"/js/{name}.js"; - }) {} + public DefaultNSResolver() : this(null) {} /// /// 使用自定义解析函数创建解析器 /// - /// 自定义路径解析函数 - /// resolveNS 为 null - public DefaultNSResolver(Func resolveNS) + /// 自定义路径解析函数 + public DefaultNSResolver(Func? customResolver) { - _resolveNS = resolveNS ?? throw new ArgumentNullException(nameof(resolveNS)); + _customResolver = customResolver; } /// - public string ResolveNS(Type tsType) => _resolveNS(tsType); + public string ResolveNS(Type tsType, string suffix) + { + if (_customResolver != null) + { + return _customResolver(tsType, suffix); + } + + // 默认解析逻辑 + var pkgName = Assembly.GetEntryAssembly()?.GetName().Name!; + var name = Regex.Replace(tsType.FullName!, $"^{pkgName}.", "").Replace(".", "/"); + return $"/js/{name}{suffix}.js"; + } } \ No newline at end of file diff --git a/BlazorTS/ScriptBridge.cs b/BlazorTS/ScriptBridge.cs index 934a518..ad5a3a1 100644 --- a/BlazorTS/ScriptBridge.cs +++ b/BlazorTS/ScriptBridge.cs @@ -76,10 +76,11 @@ public static string ResolveRazorJS(Type componentType) /// Resolves the compiled JavaScript path for TypeScript modules using the injected resolver. /// /// The TypeScript module type + /// File suffix like ".razor" or "" /// The compiled JavaScript file path - public string ResolveNS(Type tsType) + public string ResolveNS(Type tsType, string suffix) { - return resolver.ResolveNS(tsType); + return resolver.ResolveNS(tsType, suffix); } private async ValueTask Invoke(string moduleName, string methodName, params object?[]? args) diff --git a/BlazorTS/ServiceCollectionExtensions.cs b/BlazorTS/ServiceCollectionExtensions.cs index d9796d3..a41f790 100644 --- a/BlazorTS/ServiceCollectionExtensions.cs +++ b/BlazorTS/ServiceCollectionExtensions.cs @@ -26,8 +26,8 @@ public static IServiceCollection AddBlazorTS(this IServiceCollection services) /// 自定义路径解析函数 /// 服务集合 public static IServiceCollection AddBlazorTS( - this IServiceCollection services, - Func customResolver) + this IServiceCollection services, + Func customResolver) { services.AddScoped(_ => new BlazorTS.DefaultNSResolver(customResolver)); services.AddScoped(); diff --git a/README.md b/README.md index 897b953..9c99375 100644 --- a/README.md +++ b/README.md @@ -213,24 +213,43 @@ To customize paths, specify when registering services: ```csharp // Using custom function -builder.Services.AddBlazorTS(type => +builder.Services.AddBlazorTS((type, suffix) => { var path = type.FullName!.Replace('.', '/'); - return $"/scripts/{path}.js"; + return $"/scripts/{path}{suffix}.js"; }); // Using custom resolver class public class CustomResolver : INSResolver { - public string ResolveNS(Type tsType) + public string ResolveNS(Type tsType, string suffix) { var path = tsType.FullName!.Replace('.', '/'); - return $"/lib/{path}.js"; + return $"/lib/{path}{suffix}.js"; } } builder.Services.AddBlazorTS(); ``` +### Suffix Parameter + +The `ResolveNS` method now includes a `suffix` parameter to distinguish between different module types: + +- **Razor Components** (`.razor.ts` files): Use suffix `".razor"` + - `Component.razor.ts` → `/js/Component.razor.js` +- **Entry Modules** (`.entry.ts` files): Use suffix `".entry"` + - `Module.entry.ts` → `/js/Module.entry.js` +- **Custom Suffixes**: Any string can be used as suffix + +**Examples:** +```csharp +// The default resolver automatically handles suffixes +var resolver = new DefaultNSResolver(); +resolver.ResolveNS(typeof(MyComponent), ".razor"); // "/js/MyComponent.razor.js" +resolver.ResolveNS(typeof(MyModule), ".entry"); // "/js/MyModule.entry.js" +resolver.ResolveNS(typeof(MyClass), ""); // "/js/MyClass.js" +``` + ## 🔧 Supported Types | TypeScript | C# Parameter | Return Type | diff --git a/README_CN.md b/README_CN.md index ee8d192..33aa0df 100644 --- a/README_CN.md +++ b/README_CN.md @@ -216,24 +216,43 @@ BlazorTS 默认将 `MyApp.Components.Counter` 映射为 `/js/Components/Counter. ```csharp // 使用自定义函数 -builder.Services.AddBlazorTS(type => +builder.Services.AddBlazorTS((type, suffix) => { var path = type.FullName!.Replace('.', '/'); - return $"/scripts/{path}.js"; + return $"/scripts/{path}{suffix}.js"; }); // 使用自定义解析器类 public class CustomResolver : INSResolver { - public string ResolveNS(Type tsType) + public string ResolveNS(Type tsType, string suffix) { var path = tsType.FullName!.Replace('.', '/'); - return $"/lib/{path}.js"; + return $"/lib/{path}{suffix}.js"; } } builder.Services.AddBlazorTS(); ``` +### Suffix 参数 + +`ResolveNS` 方法现在包含 `suffix` 参数,用于区分不同的模块类型: + +- **Razor 组件** (`.razor.ts` 文件):使用后缀 `".razor"` + - `Component.razor.ts` → `/js/Component.razor.js` +- **Entry 模块** (`.entry.ts` 文件):使用后缀 `".entry"` + - `Module.entry.ts` → `/js/Module.entry.js` +- **自定义后缀**:可以使用任意字符串作为后缀 + +**示例:** +```csharp +// 默认解析器自动处理后缀 +var resolver = new DefaultNSResolver(); +resolver.ResolveNS(typeof(MyComponent), ".razor"); // "/js/MyComponent.razor.js" +resolver.ResolveNS(typeof(MyModule), ".entry"); // "/js/MyModule.entry.js" +resolver.ResolveNS(typeof(MyClass), ""); // "/js/MyClass.js" +``` + ## 🔧 支持的类型 | TypeScript | C# 参数 | 返回类型 | diff --git a/docs/development-guide.md b/docs/development-guide.md index c0dd716..d38915b 100644 --- a/docs/development-guide.md +++ b/docs/development-guide.md @@ -75,7 +75,7 @@ public partial class TestFunctions public class TSInterop(ScriptBridge invoker) { - private readonly string url = ScriptBridge.ResolveNS(typeof(TestFunctions)); + private readonly string url = invoker.ResolveNS(typeof(TestFunctions), ""); public async Task hello(string name) { @@ -109,6 +109,21 @@ public partial class TestFunctions | `void` | - | `Task` | | `Promise` | - | `Task` | +## Suffix 参数说明 + +`ResolveNS` 方法现在支持 `suffix` 参数来区分不同模块类型: + +```csharp +// Razor 组件:Component.razor.ts → /js/Component.razor.js +private readonly string url = invoker.ResolveNS(typeof(Component), ".razor"); + +// Entry 模块:Module.entry.ts → /js/Module.entry.js +private readonly string url = invoker.ResolveNS(typeof(Module), ".entry"); + +// 普通模块:Utils.ts → /js/Utils.js +private readonly string url = invoker.ResolveNS(typeof(Utils), ""); +``` + ## 构建和测试 ```bash