// #define UNITY_EDITOR_OSX using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using UnityEngine; using UnityEngine.Networking; using UnityEngine.Rendering; #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX using System.Linq; #endif namespace Needle.Engine.Utils { public static class NpmUtils { #if UNITY_EDITOR_WIN internal static readonly string NpmCacheDirectory = Environment.ExpandEnvironmentVariables("%AppData%/../Local/npm-cache"); #else internal static readonly string NpmCacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.npm"; #endif internal static readonly string[] NpmCacheDirectories = new string[] { NpmCacheDirectory + "/_cacache", NpmCacheDirectory + "/_npx", }; internal static readonly string NpxCacheDirectory = NpmCacheDirectory + "/_npx"; public static Task UpdatePackages(IList packages, string workingDirectory) { var cmd = "npm set registry https://registry.npmjs.org"; cmd += " && npm update " + string.Join(" ", packages); Debug.Log($"Running npm update for {packages.Count} packages\n{string.Join(", ", packages)}"); return ProcessHelper.RunCommand(cmd, workingDirectory, null, true, false); } public static string GetPackageUrl(string packageName) { return $"https://www.npmjs.com/package/{packageName}"; } public static string GetCodeUrl(string packageName, string version) { return $"https://www.npmjs.com/package/{packageName}/v/{version.AsSpecificSemVer()}?activeTab=code"; } public const string NpmRegistryUrl = "https://registry.npmjs.org/"; public const string NpmInstallFlags = "--install-links=false"; public const string NpmNoProgressAuditFundArgs = "--progress=false --no-audit --no-fund"; private static readonly char[] _rangeChars = {' ', '^', '~', 'v'}; private static readonly Regex _getSemverRegex = new Regex(@"(?\d{1,}\.\d{1,}(?\.(\d|x){1,})?(\-\w+)?(\.\d+)?)", RegexOptions.Compiled | RegexOptions.IgnoreCase); public static string AsSpecificSemVer(this string version) { if (version.StartsWith("npm:")) { // e.g. npm:@needle-tools/three@^1.2.3 version = version.Substring(4); var idx = version.LastIndexOf('@'); // split out version (e.g. ^1.2.3) var actualVersion = idx >= 0 ? version.Substring(idx + 1) : version; actualVersion = actualVersion.AsSpecificSemVer(); // concat again with prefix version = version.Substring(0, idx + 1) + actualVersion; return version; } // we use this to get the first semver in a range (e.g. <= 3.0.0 > 5.0.0-alpha would then result in 3.0.0) // https://regex101.com/r/yHZ0Jg/1 var match = _getSemverRegex.Match(version); if (match.Success) { var semver = match.Groups["semver"]; if (semver.Success) { version = semver.Value; // if a version is just "2.1" and the patch version is missing, we add a ".0" to make it a valid semver var patch = match.Groups["patch"]; if (!patch.Success) { version += ".0"; } } } return version.TrimStart(_rangeChars) .Replace("*", "0") .Replace("x", "0"); } public static string GetNpmRegistryEndpointUrlForPackage(string packageName) { return NpmRegistryUrl + "/" + packageName; } public static async Task TryGetCompletePackageInfo(string packageName) { try { using var client = new WebClient(); var json = await client.DownloadStringTaskAsync(GetNpmRegistryEndpointUrlForPackage(packageName)); return JObject.Parse(json); } catch { return null; } } public static async Task TryGetPackageVersionInfo(string packageName, string packageVersion) { try { using var client = new WebClient(); var json = await client.DownloadStringTaskAsync(GetNpmRegistryEndpointUrlForPackage(packageName) + "/" + packageVersion.AsSpecificSemVer()); return JObject.Parse(json); } catch { return null; } } public static async Task PackageExists(string packageName, string packageVersion) { try { if (packageVersion.StartsWith("^") || packageVersion.StartsWith(">") || packageVersion.StartsWith("<") || packageVersion.Contains("*") || packageVersion.Contains("x")) { var specific = packageVersion.AsSpecificSemVer(); var packageUrl = GetNpmRegistryEndpointUrlForPackage(packageName) + "/" + specific; Debug.Log($"Checking if \"{packageName}@{specific}\" exists (can not check version range yet)".LowContrast()); var res = await WebHelper.MakeHeaderOnlyRequest(packageUrl); if (res.result == UnityWebRequest.Result.Success) return true; // For when there's no patch version with a 0 (e.g. 4.4.0 vs 4.4.1) var procRes = await ProcessHelper.RunCommand($"npm view {packageName}@{packageVersion} --json", null, null, true, false); return procRes; } if (packageVersion.StartsWith("npm:")) { // In this case the package name is a scoped package name // maybe we just need to check for @ instead to cover all cases? var scopedVersionString = packageVersion.AsSpecificSemVer(); var separatorIndex = scopedVersionString.LastIndexOf('@'); packageName = scopedVersionString.Substring(0, separatorIndex); packageVersion = scopedVersionString.Substring(separatorIndex + 1); var url = GetNpmRegistryEndpointUrlForPackage(packageName) + "/" + packageVersion; var res = await WebHelper.MakeHeaderOnlyRequest(url); return res.result == UnityWebRequest.Result.Success; } else { var url = GetNpmRegistryEndpointUrlForPackage(packageName) + "/" + packageVersion.AsSpecificSemVer(); var res = await WebHelper.MakeHeaderOnlyRequest(url); return res.result == UnityWebRequest.Result.Success; } } catch { return false; } } public static async Task<(bool success, JObject rawObj)> ViewPackage(string packageName, string packageVersion) { var cmd = "npm view --json " + packageName + "@" + packageVersion; var str = ""; var success = await ProcessHelper.RunCommand(cmd, null, null, true, false, -1, default, (log, msg) => { if (!string.IsNullOrWhiteSpace(msg)) { str += msg; } }); var res = (success: success && str.StartsWith("{"), rawObj: default(JObject)); if (res.success) { var obj = JObject.Parse(str); res.rawObj = obj; } return res; } public static string GetStartCommand(string projectDirectory) { if (PackageUtils.TryGetScripts(projectDirectory + "/package.json", out var scripts)) { foreach (var kvp in scripts) { if (kvp.Value.Contains("next dev")) return "npm run " + kvp.Key; } } return "npm start"; } public static string GetInstallCommand(string projectDirectory) { if (PackageUtils.TryGetScripts(projectDirectory + "/package.json", out var scripts)) { if (scripts.TryGetValue("install", out var cmd)) return cmd; } if (File.Exists(projectDirectory + "/yarn.lock")) return $"yarn install {NpmInstallFlags} {NpmNoProgressAuditFundArgs}"; return $"npm install {NpmInstallFlags} {NpmNoProgressAuditFundArgs} --include=optional"; } public static bool IsInstallationCommand(string cmd) { if (cmd.Contains("npm install")) return true; if (cmd.Contains("npm i")) return true; if (cmd.Contains("yarn install")) return true; if (cmd.Contains("yarn add")) return true; return false; } internal static string TryFindNvmInstallDirectory() { #if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX var path = default(string); var userDirectory = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile); var npmDirectory = System.IO.Path.Combine(userDirectory, ".nvm/versions/node"); if (Directory.Exists(npmDirectory)) { var versions = System.IO.Directory.GetDirectories(npmDirectory); if (versions.Length > 0) { var latestVersion = versions.Last(); path = System.IO.Path.Combine(latestVersion, "bin"); if (!Directory.Exists(path)) path = null; } } return path; #else return null; #endif } public static void LogPaths() { #if UNITY_EDITOR_WIN var npmPaths = new List(); foreach (var line in ProcessHelper.RunCommandEnumerable("echo %PATH%")) { var parts = line.Split(';'); foreach (var part in parts) { if (part.Contains("npm") || part.Contains("nodejs")) npmPaths.Add(part); } } if (npmPaths.Count > 0) Debug.Log(string.Join("\n", npmPaths)); else Debug.Log("No npm paths found in PATH environment variable"); #endif } } }