From b80f46b6fab015d658e8bd4a34dade100eacb8b1 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:46:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(build):=20=E6=B7=BB=E5=8A=A0=20GFramework?= =?UTF-8?q?=20=E6=A8=A1=E5=9D=97=E5=8C=96=E5=85=A8=E5=B1=80=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E7=A9=BA=E9=97=B4=E5=AF=BC=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 NuGet 包中实现可选的 transitive global usings 功能 - 添加 XML 配置方式启用模块级自动命名空间导入 - 支持通过 GFrameworkExcludedUsing 排除特定命名空间 - 为所有运行时模块生成对应的 buildTransitive props 文件 - 添加 Python 脚本自动生成和验证命名空间配置 - 在文档中添加新的安装配置说明 - 创建单元测试验证生成脚本的同步状态 --- .../GFramework.Core.Abstractions.csproj | 5 + ...GeWuYou.GFramework.Core.Abstractions.props | 47 ++++ .../TransitiveGlobalUsingsGenerationTests.cs | 74 ++++++ GFramework.Core/GFramework.Core.csproj | 5 + .../GeWuYou.GFramework.Core.props | 53 +++++ .../GFramework.Ecs.Arch.Abstractions.csproj | 6 +- ...You.GFramework.Ecs.Arch.Abstractions.props | 10 + .../GFramework.Ecs.Arch.csproj | 6 +- .../GeWuYou.GFramework.Ecs.Arch.props | 13 + .../GFramework.Game.Abstractions.csproj | 5 + ...GeWuYou.GFramework.Game.Abstractions.props | 19 ++ GFramework.Game/GFramework.Game.csproj | 5 + .../GeWuYou.GFramework.Game.props | 21 ++ GFramework.Godot/GFramework.Godot.csproj | 5 + .../GeWuYou.GFramework.Godot.props | 22 ++ README.md | 26 ++ docs/zh-CN/getting-started/installation.md | 29 ++- global-usings.modules.json | 161 +++++++++++++ scripts/generate-module-global-usings.py | 222 ++++++++++++++++++ 19 files changed, 722 insertions(+), 12 deletions(-) create mode 100644 GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props create mode 100644 GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs create mode 100644 GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props create mode 100644 GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props create mode 100644 GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props create mode 100644 GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props create mode 100644 GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props create mode 100644 GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props create mode 100644 global-usings.modules.json create mode 100644 scripts/generate-module-global-usings.py diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj index f0cec86..d5f1779 100644 --- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj +++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj @@ -27,4 +27,9 @@ + + + + + diff --git a/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props b/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props new file mode 100644 index 0000000..67cca44 --- /dev/null +++ b/GFramework.Core.Abstractions/buildTransitive/GeWuYou.GFramework.Core.Abstractions.props @@ -0,0 +1,47 @@ + + + + + + <_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)"/> + + + diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs new file mode 100644 index 0000000..e840a86 --- /dev/null +++ b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsGenerationTests.cs @@ -0,0 +1,74 @@ +using System.Diagnostics; +using System.IO; + +namespace GFramework.Core.Tests.Packaging; + +/// +/// 验证模块级可选 Global Usings 的生成脚本与仓库中的已提交产物保持同步。 +/// 该测试用于防止新增模块、命名空间清单或打包声明发生漂移后静默进入仓库。 +/// +[TestFixture] +public class TransitiveGlobalUsingsGenerationTests +{ + /// + /// 验证生成脚本的检查模式可以在当前仓库状态下通过。 + /// 如果此断言失败,说明清单、生成的 props 文件或 csproj 打包声明至少有一处未同步。 + /// + [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}"); + } + + /// + /// 启动生成脚本的检查模式。 + /// 该模式不会修改仓库内容,只验证仓库中的生成产物是否已与当前规则对齐。 + /// + /// 仓库根目录。 + /// 已启动的进程实例。 + 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."); + } + + /// + /// 从测试输出目录向上回溯,定位包含解决方案文件的仓库根目录。 + /// + /// 仓库根目录绝对路径。 + 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."); + } +} \ No newline at end of file diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj index bc78025..0ef4bcd 100644 --- a/GFramework.Core/GFramework.Core.csproj +++ b/GFramework.Core/GFramework.Core.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props b/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props new file mode 100644 index 0000000..b666aa9 --- /dev/null +++ b/GFramework.Core/buildTransitive/GeWuYou.GFramework.Core.props @@ -0,0 +1,53 @@ + + + + + + <_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)"/> + + + diff --git a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj index b937843..d9550a3 100644 --- a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj +++ b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj @@ -9,5 +9,9 @@ - + + + + + diff --git a/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props b/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props new file mode 100644 index 0000000..5816a35 --- /dev/null +++ b/GFramework.Ecs.Arch.Abstractions/buildTransitive/GeWuYou.GFramework.Ecs.Arch.Abstractions.props @@ -0,0 +1,10 @@ + + + + + + <_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Include="GFramework.Ecs.Arch.Abstractions"/> + <_GFramework_Ecs_Arch_Abstractions_TransitiveUsing Remove="@(GFrameworkExcludedUsing)"/> + + + diff --git a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj index 2b58a0e..68a8916 100644 --- a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj +++ b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj @@ -19,5 +19,9 @@ - + + + + + diff --git a/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props b/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props new file mode 100644 index 0000000..1776e61 --- /dev/null +++ b/GFramework.Ecs.Arch/buildTransitive/GeWuYou.GFramework.Ecs.Arch.props @@ -0,0 +1,13 @@ + + + + + + <_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)"/> + + + diff --git a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj index 409352d..62aef63 100644 --- a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj +++ b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj @@ -28,4 +28,9 @@ runtime; build; native; contentfiles; analyzers + + + + + diff --git a/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props b/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props new file mode 100644 index 0000000..f7319ff --- /dev/null +++ b/GFramework.Game.Abstractions/buildTransitive/GeWuYou.GFramework.Game.Abstractions.props @@ -0,0 +1,19 @@ + + + + + + <_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)"/> + + + diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj index 4c3cce7..37bc8a8 100644 --- a/GFramework.Game/GFramework.Game.csproj +++ b/GFramework.Game/GFramework.Game.csproj @@ -14,4 +14,9 @@ + + + + + diff --git a/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props b/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props new file mode 100644 index 0000000..b7749c7 --- /dev/null +++ b/GFramework.Game/buildTransitive/GeWuYou.GFramework.Game.props @@ -0,0 +1,21 @@ + + + + + + <_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)"/> + + + diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj index 5d79219..44e037c 100644 --- a/GFramework.Godot/GFramework.Godot.csproj +++ b/GFramework.Godot/GFramework.Godot.csproj @@ -22,4 +22,9 @@ + + + + + diff --git a/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props b/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props new file mode 100644 index 0000000..ac14337 --- /dev/null +++ b/GFramework.Godot/buildTransitive/GeWuYou.GFramework.Godot.props @@ -0,0 +1,22 @@ + + + + + + <_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)"/> + + + diff --git a/README.md b/README.md index e9bb84a..fc7a8fe 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,32 @@ dotnet add package GeWuYou.GFramework.Godot dotnet add package GeWuYou.GFramework.SourceGenerators ``` +## 可选模块导入 + +发布后的运行时包支持可选的模块级自动导入,但默认关闭,避免在普通项目里无意污染命名空间。 + +在 NuGet 消费项目中显式开启: + +```xml + + true + +``` + +启用后,项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入其推荐命名空间。 + +如果某几个命名空间不想导入,可以局部排除: + +```xml + + + + +``` + +> 该能力面向 NuGet 包消费场景。若你在本地解决方案中直接使用 `ProjectReference`,仍建议保留自己的 `GlobalUsings.cs` 或手写 +`using`。 + ## 仓库结构 ```text diff --git a/docs/zh-CN/getting-started/installation.md b/docs/zh-CN/getting-started/installation.md index 74020b1..c6b8618 100644 --- a/docs/zh-CN/getting-started/installation.md +++ b/docs/zh-CN/getting-started/installation.md @@ -88,19 +88,28 @@ dotnet add package GeWuYou.GFramework.SourceGenerators ### 1. 基础配置 -创建 `GlobalUsings.cs` 文件: +如果你通过 NuGet 包使用 GFramework,并且希望自动导入已安装模块的推荐命名空间,可以在项目文件中显式开启: -```csharp -global using GFramework.Core; -global using GFramework.Core.Architecture; -global using GFramework.Core.Command; -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; +```xml + + true + ``` +启用后,当前项目已引用的 GFramework 运行时模块会通过 `buildTransitive` 自动注入对应命名空间。 + +如果你想排除局部导入,可以继续在项目文件中添加排除项: + +```xml + + + + + +``` + +如果你使用的是本地 `ProjectReference`,或者希望完全手动控制导入范围,仍然可以继续维护自己的 `GlobalUsings.cs` 文件。 + ### 2. Godot 项目配置 如果使用 Godot 集成,需要在项目设置中启用 C# 支持: diff --git a/global-usings.modules.json b/global-usings.modules.json new file mode 100644 index 0000000..6db53da --- /dev/null +++ b/global-usings.modules.json @@ -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" + ] + } + ] +} diff --git a/scripts/generate-module-global-usings.py b/scripts/generate-module-global-usings.py new file mode 100644 index 0000000..ba040cc --- /dev/null +++ b/scripts/generate-module-global-usings.py @@ -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_END = "" + + +@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"\s*false\s*", 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"(.*?)", 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 = [ + "", + " ", + " ", + " ", + " ", + ] + + for namespace in module.namespaces: + lines.append(f" <{item_name} Include=\"{namespace}\" />") + + lines.extend( + [ + f" <{item_name} Remove=\"@(GFrameworkExcludedUsing)\" />", + f" ", + " ", + "", + "", + ], + ) + 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}", + " ", + f" ", + " ", + 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("", f"{block}") + + 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()