Files
2025-11-30 08:35:03 +02:00

383 lines
13 KiB
C#

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<bool> 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 + "\" ?\\: ?)(?<dependencies>\\{.*?\\})";
private static readonly Dictionary<string, Regex> _blockRegexCache = new Dictionary<string, Regex>();
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\" ?: ?)(?<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\" ?: ?\"(?<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\"\\: ?\"(?<main>.+)\"");
if (main.Success)
{
mainFile = main.Groups["main"].Value;
return true;
}
mainFile = null;
return false;
}
private class PackageJsonScripts
{
public Dictionary<string, string> scripts;
}
public static bool TryGetScripts(string packageJsonPath, out Dictionary<string, string> scripts)
{
if (File.Exists(packageJsonPath))
{
var content = File.ReadAllText(packageJsonPath);
var obj = JsonConvert.DeserializeObject<PackageJsonScripts>(content);
scripts = obj?.scripts;
return scripts != null;
}
scripts = null;
return false;
}
public static bool TryWriteScripts(string packageJsonPath, Dictionary<string, string> 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<string, string> 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<Dictionary<string, string>>(match.Value);
return dict != null;
}
}
return false;
}
public static bool TryWriteBlock(string packageJsonPath, string field, Dictionary<string, string> 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;
}
/// <summary>
/// Reads the dependencies from a package.json file.
/// </summary>
/// <param name="packageJsonPath">Absolute path to package.json</param>
/// <param name="dependencies">Package-to-version</param>
/// <param name="key">The package.json entry to read, e.g. "dependencies" or "peerDependencies".</param>
/// <returns></returns>
public static bool TryReadDependencies(string packageJsonPath, out Dictionary<string, string> dependencies, string key = "dependencies")
{
return TryReadBlock(packageJsonPath, key, out dependencies);
}
public static bool TryWriteDependencies(string packageJsonPath, Dictionary<string, string> 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;
}
/// <summary>
/// Get a project relative file path for package.json
/// </summary>
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 = "\"<packageName>\" ?\\: ?\"(?<value>.*)\"";
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("<packageName>", 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) + "\" ?: ?\")(?<version>.+?)(\")";
var content = File.ReadAllText(packageJsonPath);
versionOrPath = versionOrPath.Replace("\\", "/");
content = Regex.Replace(content, pattern, "$1" + versionOrPath + "$2", RegexOptions.Singleline);
File.WriteAllText(packageJsonPath, content);
return true;
}
}
}