Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 87 additions & 11 deletions BlazorTS.SourceGenerator.Tests/INSResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<ArgumentNullException>(() => 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]
Expand All @@ -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]
Expand All @@ -68,22 +130,36 @@ 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 { }
}

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";
}
}
}
98 changes: 49 additions & 49 deletions BlazorTS.SourceGenerator/ResolveGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,28 +131,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

Helper.Log(spc, $"Processing {files.Length} BlazorTS files with nativePath: {nativePath}");

var entryModuleNames = new List<string>();
var razorInteropTypeNames = new List<string>();

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<string>();
var razorInteropTypeNames = new List<string>();

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.");

});

Expand Down Expand Up @@ -225,7 +225,7 @@ public partial class {className}
/// </summary>
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")}
}}
Expand All @@ -247,7 +247,7 @@ namespace {ns};
/// </summary>
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")}
}}
Expand Down Expand Up @@ -306,18 +306,18 @@ private static string ConvertType(string tsType)
};
}

private static string GenerateExtension(SourceProductionContext spc, List<string> entryModuleNames, List<string> 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<string> entryModuleNames, List<string> 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
{{
/// <summary>
/// Provides extension methods for setting up BlazorTS-generated script interop services.
/// </summary>
Expand All @@ -328,20 +328,20 @@ public static class ServiceCollectionExtensions
/// </summary>
/// <param name=""services"">The <see cref=""IServiceCollection""/> to add the services to.</param>
/// <returns>The <see cref=""IServiceCollection""/> so that additional calls can be chained.</returns>
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
";
}



}
}
}
34 changes: 20 additions & 14 deletions BlazorTS/INSResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,43 @@ public interface INSResolver
/// 解析 TypeScript 类型对应的 JavaScript 模块路径
/// </summary>
/// <param name="tsType">TypeScript 类型</param>
/// <param name="suffix">文件后缀,如 ".razor" 或 ""</param>
/// <returns>JavaScript 模块的 URL 路径</returns>
string ResolveNS(Type tsType);
string ResolveNS(Type tsType, string suffix);
}

/// <summary>
/// 默认的命名空间解析器实现
/// </summary>
public sealed class DefaultNSResolver : INSResolver
{
private readonly Func<Type, string> _resolveNS;
private readonly Func<Type, string, string>? _customResolver;

/// <summary>
/// 使用默认规则创建解析器:移除程序集名前缀,映射到 /js/Namespace/Foo.js
/// 使用默认规则创建解析器
/// </summary>
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) {}

/// <summary>
/// 使用自定义解析函数创建解析器
/// </summary>
/// <param name="resolveNS">自定义路径解析函数</param>
/// <exception cref="ArgumentNullException">resolveNS 为 null</exception>
public DefaultNSResolver(Func<Type, string> resolveNS)
/// <param name="customResolver">自定义路径解析函数</param>
public DefaultNSResolver(Func<Type, string, string>? customResolver)
{
_resolveNS = resolveNS ?? throw new ArgumentNullException(nameof(resolveNS));
_customResolver = customResolver;
}

/// <inheritdoc />
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";
}
}
5 changes: 3 additions & 2 deletions BlazorTS/ScriptBridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ public static string ResolveRazorJS(Type componentType)
/// Resolves the compiled JavaScript path for TypeScript modules using the injected resolver.
/// </summary>
/// <param name="tsType">The TypeScript module type</param>
/// <param name="suffix">File suffix like ".razor" or ""</param>
/// <returns>The compiled JavaScript file path</returns>
public string ResolveNS(Type tsType)
public string ResolveNS(Type tsType, string suffix)
{
return resolver.ResolveNS(tsType);
return resolver.ResolveNS(tsType, suffix);
}

private async ValueTask<TValue> Invoke<TValue>(string moduleName, string methodName, params object?[]? args)
Expand Down
4 changes: 2 additions & 2 deletions BlazorTS/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ public static IServiceCollection AddBlazorTS(this IServiceCollection services)
/// <param name="customResolver">自定义路径解析函数</param>
/// <returns>服务集合</returns>
public static IServiceCollection AddBlazorTS(
this IServiceCollection services,
Func<Type, string> customResolver)
this IServiceCollection services,
Func<Type, string, string> customResolver)
{
services.AddScoped<BlazorTS.INSResolver>(_ => new BlazorTS.DefaultNSResolver(customResolver));
services.AddScoped<BlazorTS.ScriptBridge>();
Expand Down
Loading
Loading