using System; using System.Collections.Generic; using System.IO; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Networking; namespace Needle.Engine.Utils { public static class PackageUtils { public static async Task NeedleEngineUnityVersionExists(string version) { var url = $"https://packages.needle.tools/com.needle.engine-exporter/-/com.needle.engine-exporter-{version}.tgz"; var res = await WebHelper.MakeHeaderOnlyRequest(url); return res != null && res.result == UnityWebRequest.Result.Success; } public static bool IsMajorVersionChange(string version1, string version2) { var majorIndex1 = version1.IndexOf('.'); var majorIndex2 = version2.IndexOf('.'); if(majorIndex1 < 0 || majorIndex2 < 0) return false; var major1 = version1.Substring(0, majorIndex1); var major2 = version2.Substring(0, majorIndex2); return major1 != major2; } public static bool IsChangeToPreReleaseVersion(string current, string version2) { if (current.Contains("pre") || current.Contains("exp")) { return false; } if (version2.Contains("pre") || version2.Contains("exp")) { return true; } return false; } public static bool SetVersionsToRecommendedEditorVersions(string directory, NpmUnityEditorVersions.Registry registry = NpmUnityEditorVersions.Registry.Npm) { if (!NpmUnityEditorVersions.TryGetVersions(out var versions, registry)) { Debug.LogError("No recommended versions found"); return false; } var packageJsonPath = directory + "/package.json"; var packageJsonContent = File.ReadAllText(packageJsonPath); var packageJson = JObject.Parse(packageJsonContent); var dependencies = packageJson["dependencies"] as JObject; var devDependencies = packageJson["devDependencies"] as JObject; var peerDependencies = packageJson["peerDependencies"] as JObject; foreach (var ver in versions) { var name = ver.Key; var version = ver.Value.ToString(); if (dependencies != null && dependencies.ContainsKey(name)) { dependencies[name] = version; } if (devDependencies != null && devDependencies.ContainsKey(name)) { devDependencies[name] = version; } if (peerDependencies != null && peerDependencies.ContainsKey(name)) { peerDependencies[name] = version; } } File.WriteAllText(packageJsonPath, packageJson.ToString(Formatting.Indented)); return true; } public static async void Publish(string directory, string registry = "https://registry.npmjs.org") { if (!File.Exists(directory + "/package.json")) { Debug.LogError("No package.json found in " + directory); return; } await ProcessHelper.RunCommand($"npm set registry {registry} & npm publish --access public & pause", directory, null, false); } public static bool IsMutable(string path) { if (!Directory.Exists(path) && !File.Exists(path)) return false; path = Path.GetFullPath(path); var mutable = path.Contains("Library/PackageCache") == false && path.Contains("Library\\PackageCache") == false; return mutable; } private static string GetBlockPattern(string key) => "(\"" + key + "\" ?\\: ?)(?\\{.*?\\})"; private static readonly Dictionary _blockRegexCache = new Dictionary(); private static Regex GetBlockRegex(string key) { if (_blockRegexCache.ContainsKey(key)) return _blockRegexCache[key]; var regex = new Regex(GetBlockPattern(key), RegexOptions.Singleline | RegexOptions.Compiled); _blockRegexCache.Add(key, regex); return regex; } private static readonly Regex versionRegex = new Regex("(\"version\" ?: ?)(?\".+\")", RegexOptions.Multiline | RegexOptions.Compiled); public static bool SetVersion(string packageJsonPath, string version) { if (!File.Exists(packageJsonPath)) return false; var res = versionRegex.Replace(File.ReadAllText(packageJsonPath), "$1" + "\"" + version + "\""); File.WriteAllText(packageJsonPath, res); return true; } public static bool IsLocalVersion(string versionOrPath) { return versionOrPath != null && versionOrPath.StartsWith("file"); } public static bool TryGetVersion(string packageJsonPath, out string version) { version = null; if (!File.Exists(packageJsonPath)) return false; var content = File.ReadAllText(packageJsonPath); var match = versionRegex.Match(content); if (match.Success) { version = match.Groups["version"].Value?.Trim('"'); return !string.IsNullOrWhiteSpace(version); } return false; } public static string GetPackageName(string packageJsonPath) { if (!File.Exists(packageJsonPath)) return null; var content = File.ReadAllText(packageJsonPath); var name = Regex.Match(content, "\"name\" ?: ?\"(?.+)\""); if (name.Success) return name.Groups["name"].Value; return null; } public static bool TryGetMainFile(string packageJsonPath, out string mainFile) { var content = File.ReadAllText(packageJsonPath); var main = Regex.Match(content, "\"main\"\\: ?\"(?
.+)\""); if (main.Success) { mainFile = main.Groups["main"].Value; return true; } mainFile = null; return false; } private class PackageJsonScripts { public Dictionary scripts; } public static bool TryGetScripts(string packageJsonPath, out Dictionary scripts) { if (File.Exists(packageJsonPath)) { var content = File.ReadAllText(packageJsonPath); var obj = JsonConvert.DeserializeObject(content); scripts = obj?.scripts; return scripts != null; } scripts = null; return false; } public static bool TryWriteScripts(string packageJsonPath, Dictionary scripts) { return TryWriteBlock(packageJsonPath, "scripts", scripts); } public static bool IsDependency(string packageJsonPath, string packageName) { if (!string.IsNullOrEmpty(packageName) && TryReadDependencies(packageJsonPath, out var deps)) return deps.ContainsKey(packageName); return false; } public static bool TryReadBlock(string packageJsonPath, string field, out Dictionary dict) { dict = null; if (!File.Exists(packageJsonPath)) return false; var targetJson = File.ReadAllText(packageJsonPath); var dependenciesMatch = GetBlockRegex(field).Match(targetJson); if (dependenciesMatch.Success) { var match = dependenciesMatch.Groups["dependencies"]; if (match != null && match.Success) { dict = JsonConvert.DeserializeObject>(match.Value); return dict != null; } } return false; } public static bool TryWriteBlock(string packageJsonPath, string field, Dictionary dict) { var content = File.ReadAllText(packageJsonPath); var res = JsonConvert.SerializeObject(dict, Formatting.Indented); const char indent = '\t'; res = res.Replace("\n", "\n" + indent); // $1 is referring to the "dependencies" capture (first group, 0 is ALL) var final = Regex.Replace(content, GetBlockPattern(field), $"$1{res}", RegexOptions.Singleline); File.WriteAllText(packageJsonPath, final); return true; } /// /// Reads the dependencies from a package.json file. /// /// Absolute path to package.json /// Package-to-version /// The package.json entry to read, e.g. "dependencies" or "peerDependencies". /// public static bool TryReadDependencies(string packageJsonPath, out Dictionary dependencies, string key = "dependencies") { return TryReadBlock(packageJsonPath, key, out dependencies); } public static bool TryWriteDependencies(string packageJsonPath, Dictionary dependencies, string key = "dependencies") { return TryWriteBlock(packageJsonPath, key, dependencies); } public static bool AddPackage(string targetDir, string targetPackageDir) { var targetPath = targetDir + "/package.json"; if (!File.Exists(targetPath)) return false; var packagePath = targetPackageDir + "/package.json"; if (!File.Exists(packagePath)) return false; if (TryReadDependencies(targetPath, out var dependencies)) { var name = GetPackageName(packagePath); var relPath = GetFilePath(targetDir, targetPackageDir); if (!dependencies.ContainsKey(name)) dependencies.Add(name, relPath); else dependencies[name] = relPath; return TryWriteDependencies(targetPath, dependencies); } return false; } public static bool Remove(string packageJsonPathOrDirectory, string name, string key = "dependencies") { if(!packageJsonPathOrDirectory.EndsWith("package.json")) packageJsonPathOrDirectory += "/package.json"; if (!File.Exists(packageJsonPathOrDirectory)) return false; if (TryReadDependencies(packageJsonPathOrDirectory, out var dict, key)) { dict.Remove(name); return TryWriteDependencies(packageJsonPathOrDirectory, dict, key); } return false; } /// /// Get a project relative file path for package.json /// public static string GetFilePath(string targetDir, string sourceDir) { var packageJsonUri = new Uri(targetDir + "/"); if (!Path.IsPathRooted(sourceDir)) sourceDir = Path.GetFullPath(sourceDir); var sourceUri = new Uri(sourceDir); return "file:./" + packageJsonUri.MakeRelativeUri(sourceUri).ToString().Replace("%20", " "); } public static bool IsAliasVersion(string val) { // e.g. npm:@needle-tools/engine@^0.125.0 // https://docs.npmjs.com/cli/v9/using-npm/package-spec#aliases return val.StartsWith("npm:") && val.Contains("@"); } public static bool IsPath(string val) { if (val.StartsWith("file:")) return true; if (val == "latest") return false; if (val.StartsWith("git:")) return false; // not allowed path character on windows // and it's used for explicit versions like "npm:@needle-tools/engine@^2.67.9-pre" // and possibly other registries? if(val.Contains(":")) return false; // Not entirely sure anymore why we check for the starting @ return !val.StartsWith("@") && val.Contains("/"); } public static bool TryGetPath(string dir, string val, out string fullPath) { if (IsPath(val)) { if (val.StartsWith("file:")) val = val.Substring("file:".Length); // if its already a full path if (Path.IsPathRooted(val)) { fullPath = val; return true; } // otherwise we need to reconstruct the full path relative to the directory fullPath = Path.GetFullPath(dir + "/" + val); return true; } fullPath = null; return false; } private static readonly string valueRegexPattern = "\"\" ?\\: ?\"(?.*)\""; public static bool GetDependencyValue(string packageJsonPathOrContent, string packageName, out string value) { if (string.IsNullOrWhiteSpace(packageJsonPathOrContent)) { value = null; return false; } if (File.Exists(packageJsonPathOrContent)) packageJsonPathOrContent = File.ReadAllText(packageJsonPathOrContent); var pattern = valueRegexPattern.Replace("", Regex.Escape(packageName)); var match = Regex.Match(packageJsonPathOrContent, pattern); if (match.Success) { value = match.Groups["value"].Value; return true; } value = null; return false; } public struct Options { public bool MakePathRelativeToPackageJson; } public static bool ReplacePeerDependency(string packageJsonPath, string name, string versionOrPath, Options options = default) { return Replace("peerDependencies", packageJsonPath, name, versionOrPath, options); } public static bool ReplaceDevDependency(string packageJsonPath, string name, string versionOrPath, Options options = default) { return Replace("devDependencies", packageJsonPath, name, versionOrPath, options); } public static bool ReplaceDependency(string packageJsonPath, string name, string versionOrPath, Options options = default) { return Replace("dependencies", packageJsonPath, name, versionOrPath, options); } private static bool Replace(string blockName, string packageJsonPath, string name, string versionOrPath, Options options = default) { if (options.MakePathRelativeToPackageJson) { if (Directory.Exists(versionOrPath)) { versionOrPath = new Uri(packageJsonPath).MakeRelativeUri(new Uri(Path.GetFullPath(versionOrPath))).ToString(); } } var pattern = "(" + blockName + ".+?\"" + Regex.Escape(name) + "\" ?: ?\")(?.+?)(\")"; var content = File.ReadAllText(packageJsonPath); versionOrPath = versionOrPath.Replace("\\", "/"); content = Regex.Replace(content, pattern, "$1" + versionOrPath + "$2", RegexOptions.Singleline); File.WriteAllText(packageJsonPath, content); return true; } } }