feat(build): 添加 GFramework 模块化全局命名空间导入功能

- 在 NuGet 包中实现可选的 transitive global usings 功能
- 添加 XML 配置方式启用模块级自动命名空间导入
- 支持通过 GFrameworkExcludedUsing 排除特定命名空间
- 为所有运行时模块生成对应的 buildTransitive props 文件
- 添加 Python 脚本自动生成和验证命名空间配置
- 在文档中添加新的安装配置说明
- 创建单元测试验证生成脚本的同步状态
This commit is contained in:
GeWuYou 2026-03-24 21:46:31 +08:00
parent a9ac8a927c
commit b80f46b6fa
19 changed files with 722 additions and 12 deletions

View File

@ -27,4 +27,9 @@
</PackageReference> </PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="3.0.1"/> <PackageReference Include="Mediator.Abstractions" Version="3.0.1"/>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Core.Abstractions.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,47 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Architectures"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Bases"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Command"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Concurrency"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Configuration"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Controller"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Coroutine"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Command"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Notification"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Query"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Cqrs.Request"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Data"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Enums"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Environment"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Events"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Ioc"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Lifecycle"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Localization"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Logging"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Model"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Pause"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Pool"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Properties"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Property"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Query"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Registries"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Resource"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Rule"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Serializer"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.State"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.StateManagement"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Storage"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Systems"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Time"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Utility"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Utility.Numeric"/>
<_GFramework_Core_Abstractions_TransitiveUsing Include="GFramework.Core.Abstractions.Versioning"/>
<_GFramework_Core_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Core_Abstractions_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -0,0 +1,74 @@
using System.Diagnostics;
using System.IO;
namespace GFramework.Core.Tests.Packaging;
/// <summary>
/// 验证模块级可选 Global Usings 的生成脚本与仓库中的已提交产物保持同步。
/// 该测试用于防止新增模块、命名空间清单或打包声明发生漂移后静默进入仓库。
/// </summary>
[TestFixture]
public class TransitiveGlobalUsingsGenerationTests
{
/// <summary>
/// 验证生成脚本的检查模式可以在当前仓库状态下通过。
/// 如果此断言失败,说明清单、生成的 props 文件或 csproj 打包声明至少有一处未同步。
/// </summary>
[Test]
public void GenerateModuleGlobalUsingsScript_CheckMode_Should_Pass()
{
var repositoryRoot = FindRepositoryRoot();
using var process = StartScriptCheck(repositoryRoot);
var standardOutput = process.StandardOutput.ReadToEnd();
var standardError = process.StandardError.ReadToEnd();
process.WaitForExit();
Assert.That(
process.ExitCode,
Is.EqualTo(0),
$"Expected module global usings generation to be up to date.{System.Environment.NewLine}" +
$"stdout:{System.Environment.NewLine}{standardOutput}{System.Environment.NewLine}" +
$"stderr:{System.Environment.NewLine}{standardError}");
}
/// <summary>
/// 启动生成脚本的检查模式。
/// 该模式不会修改仓库内容,只验证仓库中的生成产物是否已与当前规则对齐。
/// </summary>
/// <param name="repositoryRoot">仓库根目录。</param>
/// <returns>已启动的进程实例。</returns>
private static Process StartScriptCheck(string repositoryRoot)
{
var startInfo = new ProcessStartInfo("python3", "scripts/generate-module-global-usings.py --check")
{
WorkingDirectory = repositoryRoot,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
return Process.Start(startInfo)
?? throw new InvalidOperationException("Failed to start the module global usings generation check.");
}
/// <summary>
/// 从测试输出目录向上回溯,定位包含解决方案文件的仓库根目录。
/// </summary>
/// <returns>仓库根目录绝对路径。</returns>
private static string FindRepositoryRoot()
{
var currentDirectory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
while (currentDirectory != null)
{
var solutionPath = Path.Combine(currentDirectory.FullName, "GFramework.sln");
if (File.Exists(solutionPath))
return currentDirectory.FullName;
currentDirectory = currentDirectory.Parent;
}
throw new DirectoryNotFoundException("Could not locate the repository root for GFramework.");
}
}

View File

@ -14,4 +14,9 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5"/>
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0"/> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0"/>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Core.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,53 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Architectures"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Command"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Concurrency"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Configuration"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Constants"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine.Extensions"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Coroutine.Instructions"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Behaviors"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Command"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Notification"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Query"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Cqrs.Request"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Environment"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Events"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Events.Filters"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Extensions"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Async"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Control"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Functions"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Functional.Pipe"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Ioc"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Localization"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Localization.Formatters"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Appenders"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Filters"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Logging.Formatters"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Model"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Pause"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Pool"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Property"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Query"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Resource"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Rule"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Services"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Services.Modules"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.State"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.StateManagement"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Systems"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Time"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Utility"/>
<_GFramework_Core_TransitiveUsing Include="GFramework.Core.Utility.Numeric"/>
<_GFramework_Core_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Core_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -9,5 +9,9 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/> <ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Ecs.Arch.Abstractions.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,10 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Include="GFramework.Ecs.Arch.Abstractions"/>
<_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Ecs_Arch_Abstractions_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -19,5 +19,9 @@
<PackageReference Include="Arch" Version="2.1.0"/> <PackageReference Include="Arch" Version="2.1.0"/>
<PackageReference Include="Arch.System" Version="1.1.0"/> <PackageReference Include="Arch.System" Version="1.1.0"/>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Ecs.Arch.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,13 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch"/>
<_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Components"/>
<_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Extensions"/>
<_GFramework_Ecs_Arch_TransitiveUsing Include="GFramework.Ecs.Arch.Systems"/>
<_GFramework_Ecs_Arch_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Ecs_Arch_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -28,4 +28,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Game.Abstractions.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,19 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Asset"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Data"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Data.Events"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Enums"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Routing"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Scene"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Setting"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Setting.Data"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.Storage"/>
<_GFramework_Game_Abstractions_TransitiveUsing Include="GFramework.Game.Abstractions.UI"/>
<_GFramework_Game_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Game_Abstractions_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -14,4 +14,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4"/> <PackageReference Include="Newtonsoft.Json" Version="13.0.4"/>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Game.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,21 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Data"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Extensions"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Routing"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Scene"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Scene.Handler"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Serializer"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Setting"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Setting.Events"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.State"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.Storage"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.UI"/>
<_GFramework_Game_TransitiveUsing Include="GFramework.Game.UI.Handler"/>
<_GFramework_Game_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Game_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -22,4 +22,9 @@
<ProjectReference Include="..\GFramework.Game.Abstractions\GFramework.Game.Abstractions.csproj" PrivateAssets="all"/> <ProjectReference Include="..\GFramework.Game.Abstractions\GFramework.Game.Abstractions.csproj" PrivateAssets="all"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/> <ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
<!-- <auto-generated gframework-transitive-global-usings> -->
<ItemGroup>
<None Include="buildTransitive\GeWuYou.GFramework.Godot.props" Pack="true" PackagePath="buildTransitive" Visible="false"/>
</ItemGroup>
<!-- </auto-generated gframework-transitive-global-usings> -->
</Project> </Project>

View File

@ -0,0 +1,22 @@
<Project>
<!-- This file is generated by scripts/generate-module-global-usings.py. -->
<!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->
<!-- Add <GFrameworkExcludedUsing Include="Namespace" /> to opt out of specific namespaces. -->
<ItemGroup Condition="'$(EnableGFrameworkGlobalUsings)' == 'true'">
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Architectures"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Coroutine"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Data"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Extensions"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Extensions.Signal"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Logging"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Pause"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Pool"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Scene"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Setting"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Setting.Data"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.Storage"/>
<_GFramework_Godot_TransitiveUsing Include="GFramework.Godot.UI"/>
<_GFramework_Godot_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/>
<Using Include="@(_GFramework_Godot_TransitiveUsing)"/>
</ItemGroup>
</Project>

View File

@ -72,6 +72,32 @@ dotnet add package GeWuYou.GFramework.Godot
dotnet add package GeWuYou.GFramework.SourceGenerators dotnet add package GeWuYou.GFramework.SourceGenerators
``` ```
## 可选模块导入
发布后的运行时包支持可选的模块级自动导入,但默认关闭,避免在普通项目里无意污染命名空间。
在 NuGet 消费项目中显式开启:
```xml
<PropertyGroup>
<EnableGFrameworkGlobalUsings>true</EnableGFrameworkGlobalUsings>
</PropertyGroup>
```
启用后,项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入其推荐命名空间。
如果某几个命名空间不想导入,可以局部排除:
```xml
<ItemGroup>
<GFrameworkExcludedUsing Include="GFramework.Core.Environment" />
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions" />
</ItemGroup>
```
> 该能力面向 NuGet 包消费场景。若你在本地解决方案中直接使用 `ProjectReference`,仍建议保留自己的 `GlobalUsings.cs` 或手写
`using`
## 仓库结构 ## 仓库结构
```text ```text

View File

@ -88,19 +88,28 @@ dotnet add package GeWuYou.GFramework.SourceGenerators
### 1. 基础配置 ### 1. 基础配置
创建 `GlobalUsings.cs` 文件 如果你通过 NuGet 包使用 GFramework并且希望自动导入已安装模块的推荐命名空间可以在项目文件中显式开启
```csharp ```xml
global using GFramework.Core; <PropertyGroup>
global using GFramework.Core.Architecture; <EnableGFrameworkGlobalUsings>true</EnableGFrameworkGlobalUsings>
global using GFramework.Core.Command; </PropertyGroup>
global using GFramework.Core.Events;
global using GFramework.Core.Model;
global using GFramework.Core.Property;
global using GFramework.Core.System;
global using GFramework.Core.Utility;
``` ```
启用后,当前项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入对应命名空间。
如果你想排除局部导入,可以继续在项目文件中添加排除项:
```xml
<ItemGroup>
<GFrameworkExcludedUsing Include="GFramework.Core.Environment"/>
<GFrameworkExcludedUsing Include="GFramework.Godot.Extensions"/>
</ItemGroup>
```
如果你使用的是本地 `ProjectReference`,或者希望完全手动控制导入范围,仍然可以继续维护自己的 `GlobalUsings.cs` 文件。
### 2. Godot 项目配置 ### 2. Godot 项目配置
如果使用 Godot 集成,需要在项目设置中启用 C# 支持: 如果使用 Godot 集成,需要在项目设置中启用 C# 支持:

161
global-usings.modules.json Normal file
View File

@ -0,0 +1,161 @@
{
"modules": [
{
"project": "GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj",
"namespaces": [
"GFramework.Core.Abstractions.Architectures",
"GFramework.Core.Abstractions.Bases",
"GFramework.Core.Abstractions.Command",
"GFramework.Core.Abstractions.Concurrency",
"GFramework.Core.Abstractions.Configuration",
"GFramework.Core.Abstractions.Controller",
"GFramework.Core.Abstractions.Coroutine",
"GFramework.Core.Abstractions.Cqrs",
"GFramework.Core.Abstractions.Cqrs.Command",
"GFramework.Core.Abstractions.Cqrs.Notification",
"GFramework.Core.Abstractions.Cqrs.Query",
"GFramework.Core.Abstractions.Cqrs.Request",
"GFramework.Core.Abstractions.Data",
"GFramework.Core.Abstractions.Enums",
"GFramework.Core.Abstractions.Environment",
"GFramework.Core.Abstractions.Events",
"GFramework.Core.Abstractions.Ioc",
"GFramework.Core.Abstractions.Lifecycle",
"GFramework.Core.Abstractions.Localization",
"GFramework.Core.Abstractions.Logging",
"GFramework.Core.Abstractions.Model",
"GFramework.Core.Abstractions.Pause",
"GFramework.Core.Abstractions.Pool",
"GFramework.Core.Abstractions.Properties",
"GFramework.Core.Abstractions.Property",
"GFramework.Core.Abstractions.Query",
"GFramework.Core.Abstractions.Registries",
"GFramework.Core.Abstractions.Resource",
"GFramework.Core.Abstractions.Rule",
"GFramework.Core.Abstractions.Serializer",
"GFramework.Core.Abstractions.State",
"GFramework.Core.Abstractions.StateManagement",
"GFramework.Core.Abstractions.Storage",
"GFramework.Core.Abstractions.Systems",
"GFramework.Core.Abstractions.Time",
"GFramework.Core.Abstractions.Utility",
"GFramework.Core.Abstractions.Utility.Numeric",
"GFramework.Core.Abstractions.Versioning"
]
},
{
"project": "GFramework.Core/GFramework.Core.csproj",
"namespaces": [
"GFramework.Core.Architectures",
"GFramework.Core.Command",
"GFramework.Core.Concurrency",
"GFramework.Core.Configuration",
"GFramework.Core.Constants",
"GFramework.Core.Coroutine",
"GFramework.Core.Coroutine.Extensions",
"GFramework.Core.Coroutine.Instructions",
"GFramework.Core.Cqrs.Behaviors",
"GFramework.Core.Cqrs.Command",
"GFramework.Core.Cqrs.Notification",
"GFramework.Core.Cqrs.Query",
"GFramework.Core.Cqrs.Request",
"GFramework.Core.Environment",
"GFramework.Core.Events",
"GFramework.Core.Events.Filters",
"GFramework.Core.Extensions",
"GFramework.Core.Functional",
"GFramework.Core.Functional.Async",
"GFramework.Core.Functional.Control",
"GFramework.Core.Functional.Functions",
"GFramework.Core.Functional.Pipe",
"GFramework.Core.Ioc",
"GFramework.Core.Localization",
"GFramework.Core.Localization.Formatters",
"GFramework.Core.Logging",
"GFramework.Core.Logging.Appenders",
"GFramework.Core.Logging.Filters",
"GFramework.Core.Logging.Formatters",
"GFramework.Core.Model",
"GFramework.Core.Pause",
"GFramework.Core.Pool",
"GFramework.Core.Property",
"GFramework.Core.Query",
"GFramework.Core.Resource",
"GFramework.Core.Rule",
"GFramework.Core.Services",
"GFramework.Core.Services.Modules",
"GFramework.Core.State",
"GFramework.Core.StateManagement",
"GFramework.Core.Systems",
"GFramework.Core.Time",
"GFramework.Core.Utility",
"GFramework.Core.Utility.Numeric"
]
},
{
"project": "GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj",
"namespaces": [
"GFramework.Game.Abstractions.Asset",
"GFramework.Game.Abstractions.Data",
"GFramework.Game.Abstractions.Data.Events",
"GFramework.Game.Abstractions.Enums",
"GFramework.Game.Abstractions.Routing",
"GFramework.Game.Abstractions.Scene",
"GFramework.Game.Abstractions.Setting",
"GFramework.Game.Abstractions.Setting.Data",
"GFramework.Game.Abstractions.Storage",
"GFramework.Game.Abstractions.UI"
]
},
{
"project": "GFramework.Game/GFramework.Game.csproj",
"namespaces": [
"GFramework.Game.Data",
"GFramework.Game.Extensions",
"GFramework.Game.Routing",
"GFramework.Game.Scene",
"GFramework.Game.Scene.Handler",
"GFramework.Game.Serializer",
"GFramework.Game.Setting",
"GFramework.Game.Setting.Events",
"GFramework.Game.State",
"GFramework.Game.Storage",
"GFramework.Game.UI",
"GFramework.Game.UI.Handler"
]
},
{
"project": "GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj",
"namespaces": [
"GFramework.Ecs.Arch.Abstractions"
]
},
{
"project": "GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj",
"namespaces": [
"GFramework.Ecs.Arch",
"GFramework.Ecs.Arch.Components",
"GFramework.Ecs.Arch.Extensions",
"GFramework.Ecs.Arch.Systems"
]
},
{
"project": "GFramework.Godot/GFramework.Godot.csproj",
"namespaces": [
"GFramework.Godot.Architectures",
"GFramework.Godot.Coroutine",
"GFramework.Godot.Data",
"GFramework.Godot.Extensions",
"GFramework.Godot.Extensions.Signal",
"GFramework.Godot.Logging",
"GFramework.Godot.Pause",
"GFramework.Godot.Pool",
"GFramework.Godot.Scene",
"GFramework.Godot.Setting",
"GFramework.Godot.Setting.Data",
"GFramework.Godot.Storage",
"GFramework.Godot.UI"
]
}
]
}

View File

@ -0,0 +1,222 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
from dataclasses import dataclass
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parent.parent
CONFIG_PATH = ROOT_DIR / "global-usings.modules.json"
AUTO_GENERATED_START = "<!-- <auto-generated gframework-transitive-global-usings> -->"
AUTO_GENERATED_END = "<!-- </auto-generated gframework-transitive-global-usings> -->"
@dataclass(frozen=True)
class ModuleConfig:
project_path: Path
namespaces: tuple[str, ...]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate optional transitive global usings for packable GFramework runtime modules.",
)
parser.add_argument(
"--check",
action="store_true",
help="Validate that generated files are up to date instead of writing them.",
)
return parser.parse_args()
def load_text(path: Path) -> tuple[str, str]:
raw = path.read_bytes()
if raw.startswith(b"\xef\xbb\xbf"):
return raw.decode("utf-8-sig"), "utf-8-sig"
return raw.decode("utf-8"), "utf-8"
def write_text(path: Path, content: str, encoding: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding=encoding)
def load_modules() -> list[ModuleConfig]:
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
modules: list[ModuleConfig] = []
for entry in data["modules"]:
project_path = ROOT_DIR / entry["project"]
namespaces = tuple(dict.fromkeys(entry["namespaces"]))
modules.append(ModuleConfig(project_path=project_path, namespaces=namespaces))
return modules
def discover_runtime_modules() -> list[Path]:
projects: list[Path] = []
for project_path in sorted(ROOT_DIR.rglob("*.csproj")):
if "obj" in project_path.parts or "bin" in project_path.parts:
continue
if project_path.name == "GFramework.csproj":
continue
project_name = project_path.stem
if not project_name.startswith("GFramework."):
continue
if ".Tests" in project_name or "SourceGenerators" in project_name:
continue
csproj_text, _ = load_text(project_path)
if re.search(r"<IsPackable>\s*false\s*</IsPackable>", csproj_text, re.IGNORECASE):
continue
projects.append(project_path)
return projects
def resolve_package_id(project_path: Path) -> str:
project_text, _ = load_text(project_path)
match = re.search(r"<PackageId>(.*?)</PackageId>", project_text, re.DOTALL)
if match is None:
return project_path.stem
package_id = match.group(1).strip()
return package_id.replace("$(AssemblyName)", project_path.stem)
def sanitize_identifier(text: str) -> str:
return re.sub(r"[^A-Za-z0-9_]", "_", text)
def render_props(module: ModuleConfig) -> str:
item_name = f"_{sanitize_identifier(module.project_path.stem)}_TransitiveUsing"
lines = [
"<Project>",
" <!-- This file is generated by scripts/generate-module-global-usings.py. -->",
" <!-- EnableGFrameworkGlobalUsings=true enables the transitive global usings from this package. -->",
" <!-- Add <GFrameworkExcludedUsing Include=\"Namespace\" /> to opt out of specific namespaces. -->",
" <ItemGroup Condition=\"'$(EnableGFrameworkGlobalUsings)' == 'true'\">",
]
for namespace in module.namespaces:
lines.append(f" <{item_name} Include=\"{namespace}\" />")
lines.extend(
[
f" <{item_name} Remove=\"@(GFrameworkExcludedUsing)\" />",
f" <Using Include=\"@({item_name})\" />",
" </ItemGroup>",
"</Project>",
"",
],
)
return "\n".join(lines)
def render_pack_block(package_id: str) -> str:
props_path = f"buildTransitive\\{package_id}.props"
return "\n".join(
[
f" {AUTO_GENERATED_START}",
" <ItemGroup>",
f" <None Include=\"{props_path}\" Pack=\"true\" PackagePath=\"buildTransitive\" Visible=\"false\"/>",
" </ItemGroup>",
f" {AUTO_GENERATED_END}",
"",
],
)
def update_csproj(project_path: Path, package_id: str) -> tuple[str, str]:
project_text, encoding = load_text(project_path)
block = render_pack_block(package_id)
pattern = re.compile(
rf"\s*{re.escape(AUTO_GENERATED_START)}.*?{re.escape(AUTO_GENERATED_END)}\s*",
re.DOTALL,
)
if pattern.search(project_text):
updated = pattern.sub(lambda _: f"\n{block}", project_text)
else:
updated = project_text.replace("</Project>", f"{block}</Project>")
return updated, encoding
def validate_module_coverage(modules: list[ModuleConfig]) -> None:
configured_projects = {module.project_path.resolve() for module in modules}
discovered_projects = {project.resolve() for project in discover_runtime_modules()}
missing = sorted(project.relative_to(ROOT_DIR) for project in discovered_projects - configured_projects)
extra = sorted(project.relative_to(ROOT_DIR) for project in configured_projects - discovered_projects)
if missing or extra:
messages: list[str] = []
if missing:
messages.append("Unconfigured runtime modules:\n - " + "\n - ".join(str(path) for path in missing))
if extra:
messages.append("Configured modules that are not eligible runtime packages:\n - " +
"\n - ".join(str(path) for path in extra))
raise SystemExit("\n".join(messages))
def check_or_write(path: Path, expected: str, encoding: str, check_only: bool, changed: list[Path]) -> None:
if path.exists():
current, _ = load_text(path)
if current == expected:
return
elif check_only:
changed.append(path)
return
if check_only:
changed.append(path)
return
write_text(path, expected, encoding)
changed.append(path)
def main() -> None:
args = parse_args()
modules = load_modules()
validate_module_coverage(modules)
changed: list[Path] = []
for module in modules:
package_id = resolve_package_id(module.project_path)
props_path = module.project_path.parent / "buildTransitive" / f"{package_id}.props"
props_content = render_props(module)
check_or_write(props_path, props_content, "utf-8", args.check, changed)
updated_project, encoding = update_csproj(module.project_path, package_id)
check_or_write(module.project_path, updated_project, encoding, args.check, changed)
if args.check:
if changed:
relative_paths = "\n".join(f" - {path.relative_to(ROOT_DIR)}" for path in changed)
raise SystemExit(f"Generated module global usings are out of date:\n{relative_paths}")
print("Module global usings are up to date.")
return
if changed:
for path in changed:
print(f"Updated {path.relative_to(ROOT_DIR)}")
return
print("No module global usings changes were needed.")
if __name__ == "__main__":
main()