#if UNITY_EDITOR #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; namespace ShrinkSDK.WorkspaceValidation { public static class ShrinkSdkWorkspaceValidation { private static readonly ExpectedPackage[] ExpectedPackages = { new ExpectedPackage("ShrinkApp.Core", "com.cneicy.shrink-app-core", "0.1.2"), new ExpectedPackage("ShrinkApp.Starter.Basic", "com.cneicy.shrink-app-starter-basic", "0.2.0"), new ExpectedPackage("ShrinkCommand", "com.cneicy.shrink-command", "0.2.0"), new ExpectedPackage("ShrinkCommand.Integration.App", "com.cneicy.shrink-command-integration-app", "0.1.1"), new ExpectedPackage("ShrinkCommand.Integration.EventBus", "com.cneicy.shrink-command-integration-eventbus", "0.1.1"), new ExpectedPackage("ShrinkCommand.Integration.Network", "com.cneicy.shrink-command-integration-network", "0.1.1"), new ExpectedPackage("ShrinkContext.AppAdapter", "com.cneicy.shrink-context-app-adapter", "0.1.3"), new ExpectedPackage("ShrinkContext.Core", "com.cneicy.shrink-context-core", "0.1.0"), new ExpectedPackage("ShrinkContext.EventBusAdapter", "com.cneicy.shrink-context-eventbus-adapter", "0.1.0"), new ExpectedPackage("ShrinkDataSaver", "com.cneicy.shrink-datasaver", "2.2.1"), new ExpectedPackage("ShrinkDataSaver.Integration.App", "com.cneicy.shrink-datasaver-integration-app", "0.1.1"), new ExpectedPackage("ShrinkDataSaver.Integration.EventBus", "com.cneicy.shrink-datasaver-integration-eventbus", "2.1.1"), new ExpectedPackage("ShrinkEventBus", "com.cneicy.shrink-eventbus", "2.0.0"), new ExpectedPackage("ShrinkEventBus.Entities", "com.cneicy.shrink-eventbus-entities", "0.1.0"), new ExpectedPackage("ShrinkInstaller", "com.cneicy.shrink-installer", "0.2.2"), new ExpectedPackage("ShrinkModFramework", "com.cneicy.shrink-mod-framework", "0.2.2"), new ExpectedPackage("ShrinkNetwork", "com.cneicy.shrink-network", "0.2.1"), new ExpectedPackage("ShrinkNetwork.Integration.App", "com.cneicy.shrink-network-integration-app", "0.1.1"), new ExpectedPackage("ShrinkNetwork.Integration.EventBus", "com.cneicy.shrink-network-integration-eventbus", "0.1.2"), new ExpectedPackage("ShrinkShared.CodeGen", "com.cneicy.shrink-shared-codegen", "0.1.0"), new ExpectedPackage("ShrinkTutorial", "com.cneicy.shrink-tutorial", "0.1.2") }; public static void Run() { try { var workspaceRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); var packages = ReadPackages(workspaceRoot); ValidateGraph(packages); Debug.Log($"ShrinkSDK Workspace validation passed: packages={packages.Count}"); EditorApplication.Exit(0); } catch (Exception exception) { Debug.LogException(exception); EditorApplication.Exit(1); } } private static Dictionary ReadPackages(string workspaceRoot) { var result = new Dictionary(StringComparer.Ordinal); foreach (var expected in ExpectedPackages) { var manifestPath = Path.Combine(workspaceRoot, "Assets", "Modules", expected.Directory, "package.json"); if (!File.Exists(manifestPath)) { throw new FileNotFoundException($"Required package manifest is missing: {expected.Directory}", manifestPath); } var manifest = JObject.Parse(File.ReadAllText(manifestPath)); var packageName = manifest.Value("name"); var version = manifest.Value("version"); if (packageName == null || version == null || packageName.Length == 0 || version.Length == 0) { throw new InvalidOperationException($"{expected.Directory} has no valid package name or version."); } if (!string.Equals(packageName, expected.Name, StringComparison.Ordinal) || !string.Equals(version, expected.Version, StringComparison.Ordinal)) { throw new InvalidOperationException( $"{expected.Directory} must be {expected.Name}@{expected.Version}, found {packageName}@{version}."); } if (result.ContainsKey(packageName)) { throw new InvalidOperationException($"Duplicate package manifest name: {packageName}"); } result.Add(packageName, new PackageDefinition(packageName, version, manifest)); } return result; } private static void ValidateGraph(IReadOnlyDictionary packages) { var dependencies = new Dictionary>(StringComparer.Ordinal); foreach (var package in packages.Values) { var localDependencies = new HashSet(StringComparer.Ordinal); var manifestDependencies = package.Manifest["dependencies"] as JObject; if (manifestDependencies != null) { foreach (var property in manifestDependencies.Properties()) { if (!packages.TryGetValue(property.Name, out var dependency)) { continue; } var expectedVersion = property.Value.Value(); if (!string.Equals(expectedVersion, dependency.Version, StringComparison.Ordinal)) { throw new InvalidOperationException( $"{package.Name} requires {dependency.Name} {expectedVersion}, but the Workspace provides {dependency.Version}."); } var isIntegrationOrStarter = package.Name.IndexOf("-integration-", StringComparison.Ordinal) >= 0 || package.Name.IndexOf("-starter-", StringComparison.Ordinal) >= 0; if (dependency.Name.IndexOf("-integration-", StringComparison.Ordinal) >= 0 && !isIntegrationOrStarter) { throw new InvalidOperationException( $"{package.Name} must not depend on integration package {dependency.Name}."); } localDependencies.Add(dependency.Name); } } dependencies.Add(package.Name, localDependencies); } var resolved = new HashSet(StringComparer.Ordinal); var madeProgress = true; while (madeProgress) { madeProgress = false; foreach (var packageName in dependencies.Keys.OrderBy(name => name, StringComparer.Ordinal)) { if (resolved.Contains(packageName) || dependencies[packageName].Any(dependency => !resolved.Contains(dependency))) { continue; } resolved.Add(packageName); madeProgress = true; } } if (resolved.Count != packages.Count) { var blocked = dependencies .Where(pair => !resolved.Contains(pair.Key)) .Select(pair => pair.Key + " -> " + string.Join(", ", pair.Value.Where(dependency => !resolved.Contains(dependency)))) .OrderBy(value => value, StringComparer.Ordinal); throw new InvalidOperationException("Circular internal package dependencies detected: " + string.Join("; ", blocked)); } } private sealed class PackageDefinition { public PackageDefinition(string name, string version, JObject manifest) { Name = name; Version = version; Manifest = manifest; } public string Name { get; } public string Version { get; } public JObject Manifest { get; } } private readonly struct ExpectedPackage { public ExpectedPackage(string directory, string name, string version) { Directory = directory; Name = name; Version = version; } public string Directory { get; } public string Name { get; } public string Version { get; } } } } #endif