diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..1b1ad0a
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ (global::System.StringComparer.Ordinal);
+ var exactExclusions = new global::System.Collections.Generic.HashSet(global::System.StringComparer.Ordinal);
+ var prefixExclusions = new global::System.Collections.Generic.List();
+ var namespacePattern = new global::System.Text.RegularExpressions.Regex(
+ @"^\s*namespace\s+([A-Za-z_][A-Za-z0-9_.]*)\s*(?:;|\{)",
+ global::System.Text.RegularExpressions.RegexOptions.Compiled);
+
+ if (ExcludedNamespaces != null)
+ {
+ foreach (var excludedNamespace in ExcludedNamespaces)
+ {
+ if (!string.IsNullOrWhiteSpace(excludedNamespace.ItemSpec))
+ {
+ exactExclusions.Add(excludedNamespace.ItemSpec.Trim());
+ }
+ }
+ }
+
+ if (ExcludedNamespacePrefixes != null)
+ {
+ foreach (var excludedPrefix in ExcludedNamespacePrefixes)
+ {
+ if (!string.IsNullOrWhiteSpace(excludedPrefix.ItemSpec))
+ {
+ prefixExclusions.Add(excludedPrefix.ItemSpec.Trim());
+ }
+ }
+ }
+
+ foreach (var sourceFile in SourceFiles)
+ {
+ var path = sourceFile.ItemSpec;
+ if (!global::System.IO.File.Exists(path))
+ {
+ continue;
+ }
+
+ foreach (var line in global::System.IO.File.ReadLines(path))
+ {
+ var match = namespacePattern.Match(line);
+ if (!match.Success)
+ {
+ continue;
+ }
+
+ var namespaceName = match.Groups[1].Value;
+ if (!namespaceName.StartsWith("GFramework.", global::System.StringComparison.Ordinal))
+ {
+ continue;
+ }
+
+ if (exactExclusions.Contains(namespaceName))
+ {
+ continue;
+ }
+
+ var excludedByPrefix = false;
+ foreach (var prefix in prefixExclusions)
+ {
+ if (namespaceName.StartsWith(prefix, global::System.StringComparison.Ordinal))
+ {
+ excludedByPrefix = true;
+ break;
+ }
+ }
+
+ if (!excludedByPrefix)
+ {
+ discoveredNamespaces.Add(namespaceName);
+ }
+ }
+ }
+
+ static string Escape(string value)
+ {
+ return global::System.Security.SecurityElement.Escape(value) ?? value;
+ }
+
+ var directory = global::System.IO.Path.GetDirectoryName(OutputFile);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ global::System.IO.Directory.CreateDirectory(directory);
+ }
+
+ var builder = new global::System.Text.StringBuilder();
+ var msbuildPropertyOpen = new string(new[] { '$', '(' });
+ var msbuildItemOpen = new string(new[] { '@', '(' });
+ builder.AppendLine("");
+ builder.AppendLine(" ");
+ builder.AppendLine(" ");
+ builder.AppendLine(" ");
+ builder.Append(" ");
+
+ foreach (var namespaceName in discoveredNamespaces)
+ {
+ builder.Append(" <");
+ builder.Append(NamespaceItemName);
+ builder.Append(" Include=\"");
+ builder.Append(Escape(namespaceName));
+ builder.AppendLine("\" />");
+ }
+
+ builder.Append(" <");
+ builder.Append(NamespaceItemName);
+ builder.Append(" Remove=\"");
+ builder.Append(msbuildItemOpen);
+ builder.AppendLine("GFrameworkExcludedUsing)\" />");
+ builder.Append(" ");
+ builder.AppendLine(" ");
+ builder.AppendLine("");
+
+ global::System.IO.File.WriteAllText(OutputFile, builder.ToString(), new global::System.Text.UTF8Encoding(false));
+ Log.LogMessage(global::Microsoft.Build.Framework.MessageImportance.Low,
+ $"Generated {discoveredNamespaces.Count} transitive global usings for {OutputFile}.");
+ ]]>
+
+
+
+
+ <_GFrameworkTransitiveGlobalUsingsEnabled Condition="'$(EnableGFrameworkPackageTransitiveGlobalUsings)' == 'true' and '$(IsPackable)' != 'false'">true
+ <_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(TargetFrameworks)' != ''">$([System.String]::Copy('$(TargetFrameworks)').Split(';')[0])
+ <_GFrameworkTransitiveGlobalUsingsGenerationBuild Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and ('$(TargetFrameworks)' == '' or '$(TargetFramework)' == '$(_GFrameworkTransitiveGlobalUsingsPrimaryTargetFramework)')">true
+ <_GFrameworkTransitiveGlobalUsingsPackageId Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(PackageId)' != ''">$(PackageId)
+ <_GFrameworkTransitiveGlobalUsingsPackageId Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true' and '$(_GFrameworkTransitiveGlobalUsingsPackageId)' == ''">$(AssemblyName)
+ <_GFrameworkTransitiveGlobalUsingsOutputFile Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">$(BaseIntermediateOutputPath)gframework/$(_GFrameworkTransitiveGlobalUsingsPackageId).props
+ <_GFrameworkTransitiveGlobalUsingsItemName Condition="'$(_GFrameworkTransitiveGlobalUsingsEnabled)' == 'true'">_$([System.Text.RegularExpressions.Regex]::Replace('$(MSBuildProjectName)', '[^A-Za-z0-9_]', '_'))_TransitiveUsing
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj
index f0cec86..83dea9d 100644
--- a/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj
+++ b/GFramework.Core.Abstractions/GFramework.Core.Abstractions.csproj
@@ -10,6 +10,7 @@
T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute
enable
true
+ true
diff --git a/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs
new file mode 100644
index 0000000..78bda4b
--- /dev/null
+++ b/GFramework.Core.Tests/Packaging/TransitiveGlobalUsingsPackagingTests.cs
@@ -0,0 +1,80 @@
+using System.IO;
+using System.Runtime.CompilerServices;
+using GFramework.Core.Architectures;
+using GFramework.Core.Coroutine.Extensions;
+
+namespace GFramework.Core.Tests.Packaging;
+
+///
+/// 验证运行时模块在构建期间会自动生成 transitive global usings 资产。
+/// 该测试覆盖命名空间自动发现、框架侧过滤和消费者侧排除钩子的最终构建产物。
+///
+[TestFixture]
+public class TransitiveGlobalUsingsPackagingTests
+{
+ ///
+ /// 使用真实类型派生架构命名空间,避免测试断言和命名空间重构脱节。
+ ///
+ private static readonly string ArchitectureNamespace = typeof(Architecture).Namespace
+ ?? throw new InvalidOperationException(
+ "Architecture namespace should not be null.");
+
+ ///
+ /// 使用真实类型派生扩展命名空间,避免对字面量命名空间字符串的重复维护。
+ ///
+ private static readonly string ExtensionsNamespace = typeof(ContextAwareEnvironmentExtensions).Namespace
+ ?? throw new InvalidOperationException(
+ "Extensions namespace should not be null.");
+
+ ///
+ /// 使用真实类型派生协程扩展命名空间,确保断言和源码自动发现保持一致。
+ ///
+ private static readonly string CoroutineExtensionsNamespace = typeof(CoroutineExtensions).Namespace
+ ?? throw new InvalidOperationException(
+ "Coroutine extensions namespace should not be null.");
+
+ ///
+ /// 验证 GFramework.Core 在构建后会生成 transitive global usings props,
+ /// 且 props 内容来自源码自动发现,并保留消费者侧排除机制。
+ ///
+ [Test]
+ public void CoreBuild_Should_Generate_AutoDiscovered_TransitiveGlobalUsingsProps()
+ {
+ var repositoryRoot = ResolveRepositoryRoot();
+ var propsPath = Path.Combine(
+ repositoryRoot,
+ "GFramework.Core",
+ "obj",
+ "gframework",
+ "GeWuYou.GFramework.Core.props");
+
+ Assert.That(File.Exists(propsPath), Is.True, $"Expected generated props to exist: {propsPath}");
+ var propsContent = File.ReadAllText(propsPath);
+
+ Assert.That(propsContent, Does.Contain(ExtensionsNamespace));
+ Assert.That(propsContent, Does.Contain(ArchitectureNamespace));
+ Assert.That(propsContent, Does.Contain(CoroutineExtensionsNamespace));
+ Assert.That(propsContent, Does.Contain("Remove=\"@(GFrameworkExcludedUsing)\""));
+ Assert.That(propsContent, Does.Not.Contain("System.Runtime.CompilerServices"));
+ }
+
+ ///
+ /// 基于当前测试源文件的已知位置解析仓库根目录。
+ /// 这里不扫描解决方案文件,避免测试对仓库布局演进产生额外脆弱性。
+ ///
+ /// 由编译器注入的当前测试源文件绝对路径。
+ /// 仓库根目录绝对路径。
+ private static string ResolveRepositoryRoot([CallerFilePath] string sourceFilePath = "")
+ {
+ if (string.IsNullOrWhiteSpace(sourceFilePath))
+ {
+ throw new InvalidOperationException("Caller file path is required to resolve the repository root.");
+ }
+
+ var sourceDirectory = Path.GetDirectoryName(sourceFilePath)
+ ?? throw new DirectoryNotFoundException(
+ $"Could not determine the directory for source file path: {sourceFilePath}");
+
+ return Path.GetFullPath(Path.Combine(sourceDirectory, "..", ".."));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/GFramework.Core.csproj b/GFramework.Core/GFramework.Core.csproj
index bc78025..c450b44 100644
--- a/GFramework.Core/GFramework.Core.csproj
+++ b/GFramework.Core/GFramework.Core.csproj
@@ -6,6 +6,7 @@
disable
enable
true
+ true
diff --git a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj
index b937843..94337e8 100644
--- a/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj
+++ b/GFramework.Ecs.Arch.Abstractions/GFramework.Ecs.Arch.Abstractions.csproj
@@ -4,10 +4,10 @@
GeWuYou.$(AssemblyName)
true
enable
+ true
-
diff --git a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj
index 2b58a0e..a79bec7 100644
--- a/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj
+++ b/GFramework.Ecs.Arch/GFramework.Ecs.Arch.csproj
@@ -7,6 +7,7 @@
enable
true
true
+ true
@@ -19,5 +20,4 @@
-
diff --git a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj
index 409352d..b4a8890 100644
--- a/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj
+++ b/GFramework.Game.Abstractions/GFramework.Game.Abstractions.csproj
@@ -10,6 +10,7 @@
T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute
enable
true
+ true
diff --git a/GFramework.Game/GFramework.Game.csproj b/GFramework.Game/GFramework.Game.csproj
index 4c3cce7..5bb94d9 100644
--- a/GFramework.Game/GFramework.Game.csproj
+++ b/GFramework.Game/GFramework.Game.csproj
@@ -6,6 +6,7 @@
disable
enable
true
+ true
diff --git a/GFramework.Godot/GFramework.Godot.csproj b/GFramework.Godot/GFramework.Godot.csproj
index 5d79219..29c065c 100644
--- a/GFramework.Godot/GFramework.Godot.csproj
+++ b/GFramework.Godot/GFramework.Godot.csproj
@@ -6,6 +6,7 @@
enable
net8.0;net9.0;net10.0
true
+ true
$(MSBuildProjectDirectory)
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# 支持: