482 lines
15 KiB
C#
482 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using JetBrains.Annotations;
|
|
using Needle.Engine.Codegen;
|
|
using Needle.Engine.Problems;
|
|
using Needle.Engine.Utils;
|
|
using Newtonsoft.Json;
|
|
using UnityEditor;
|
|
using UnityEditor.PackageManager;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Needle.Engine.ProjectBundle
|
|
{
|
|
public enum BundleType
|
|
{
|
|
Embedded = 0,
|
|
Local = 1,
|
|
Remote= 2,
|
|
}
|
|
|
|
// can not be a scriptable object because this should not require a dependency to or core package
|
|
public class Bundle
|
|
{
|
|
internal static bool TryGetFromPath(string dirOrNpmdefPath, out Bundle res)
|
|
{
|
|
if (File.Exists(dirOrNpmdefPath))
|
|
{
|
|
// try to find the registered bundle for it (which is external) and matches the path
|
|
var npmdefPath = new FileInfo(dirOrNpmdefPath);
|
|
foreach (var bundle in BundleRegistry.Instance.Bundles)
|
|
{
|
|
var otherNpmdefPath = new FileInfo(bundle.FilePath);
|
|
if (otherNpmdefPath.Exists && otherNpmdefPath.FullName == npmdefPath.FullName)
|
|
{
|
|
res = bundle;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
res = null;
|
|
return false;
|
|
}
|
|
|
|
[JsonIgnore] public string Name => System.IO.Path.GetFileNameWithoutExtension(FilePath) + "~";
|
|
|
|
[JsonProperty] internal BundleType type;
|
|
|
|
[JsonProperty] internal string packageName;
|
|
[JsonProperty] internal string packageVersion;
|
|
|
|
/// <summary>
|
|
/// If an npmdef does not exist next to this Asset this is the path to the package (project relative if possible)
|
|
/// </summary>
|
|
[JsonProperty] internal string localPath;
|
|
|
|
/// <summary>
|
|
/// Allow to run codegen for this bundle
|
|
/// </summary>
|
|
[JsonProperty] internal bool allowCodegen = true;
|
|
|
|
private static string _lastSelectedPath
|
|
{
|
|
get => SessionState.GetString("needle.move.npmdef", "");
|
|
set => SessionState.SetString("needle.move.npmdef", value);
|
|
}
|
|
|
|
internal void SetLocalPath(string path)
|
|
{
|
|
// We can not modify the package path here because it might be inside a tgz/PackageCache!
|
|
// E.g. when the user selects a local directory it should be saved as is
|
|
// BUT better would be to have this relative
|
|
localPath = path;// path.RelativeTo(Path.GetDirectoryName(Path.GetFullPath(_filePath)));
|
|
}
|
|
|
|
internal void RemoveExternalPath()
|
|
{
|
|
localPath = null;
|
|
}
|
|
|
|
internal bool SelectLocalPath()
|
|
{
|
|
var dialoguePath = _lastSelectedPath;
|
|
if (this.IsLocal || string.IsNullOrWhiteSpace(_lastSelectedPath))
|
|
{
|
|
if(Directory.Exists(PackageDirectory))
|
|
dialoguePath = PackageDirectory;
|
|
}
|
|
var selectedPath = EditorUtility.OpenFolderPanel("Select npm package", dialoguePath, "");
|
|
if (string.IsNullOrEmpty(selectedPath))
|
|
{
|
|
Debug.Log("Selecting external path cancelled.");
|
|
return false;
|
|
}
|
|
_lastSelectedPath = selectedPath;
|
|
if (File.Exists(selectedPath + "/package.json"))
|
|
{
|
|
Debug.Log("Selected directory: " + selectedPath);
|
|
SetLocalPath(selectedPath);
|
|
Save(FilePath);
|
|
BundleRegistry.Instance.RunCodeGenForBundle(this);
|
|
return true;
|
|
}
|
|
var dirInfo = new DirectoryInfo(selectedPath);
|
|
var oldPath = PackageDirectory != null ? new DirectoryInfo(PackageDirectory) : null;
|
|
if(dirInfo.Exists && oldPath != null)
|
|
{
|
|
if (oldPath.FullName == dirInfo.FullName)
|
|
{
|
|
Debug.Log("Selected directory is the same as the current one - nothing to do here: " + dirInfo.FullName);
|
|
return true;
|
|
}
|
|
var unityProjectDir = new DirectoryInfo(Application.dataPath);
|
|
var isInUnityProject = dirInfo.FullName.StartsWith(unityProjectDir!.FullName);
|
|
var needsInstall = true;
|
|
|
|
Engine.Actions.StopLocalServer();
|
|
if (oldPath.Exists)
|
|
{
|
|
var targetPath = selectedPath + "/" + oldPath.Name;
|
|
// Make sure if moving the package into the unity directory that it ends with a ~
|
|
if (isInUnityProject && !targetPath.EndsWith("~"))
|
|
{
|
|
Debug.LogWarning("The selected directory is inside the Unity project → we will move the package to a hidden folder (ending with ~) so that Unity will not import all the files in node_modules");
|
|
targetPath += "~";
|
|
}
|
|
if (Directory.Exists(targetPath))
|
|
{
|
|
// If the target path already exists and is NOT empty we can not move there
|
|
if (new DirectoryInfo(targetPath).EnumerateFileSystemInfos().Any())
|
|
{
|
|
Debug.LogError("Selected directory already contains a folder named " + oldPath.Name +
|
|
". Please select a different location or remove the folder at " +
|
|
targetPath);
|
|
return false;
|
|
}
|
|
// otherwise we have to delete the empty directory to be able to move the package to it
|
|
Directory.Delete(targetPath);
|
|
}
|
|
selectedPath = targetPath;
|
|
if (!FileUtils.MoveFiles(oldPath.FullName, targetPath))
|
|
{
|
|
Debug.LogError("Moving package from \"" + oldPath + "\" to \"" + selectedPath + "\" failed. You have to move it manually.");
|
|
EditorUtility.RevealInFinder(oldPath.FullName);
|
|
}
|
|
else Debug.Log("Moved package to " + selectedPath);
|
|
}
|
|
else
|
|
{
|
|
if (ProjectWindowActions.DoesUserWantToCreateANewNpmPackageAtSelectedPath(selectedPath))
|
|
{
|
|
needsInstall = false;
|
|
ProjectWindowActions.CreateNewNpmPackageForLinkedBundle(this, selectedPath);
|
|
}
|
|
else
|
|
{
|
|
Debug.Log("Selected directory is not a npm package: it does not contain a package.json\n" + selectedPath);
|
|
return false;
|
|
}
|
|
}
|
|
localPath = PathUtils.MakeProjectRelative(selectedPath);
|
|
Save(FilePath);
|
|
if (allowCodegen && type != BundleType.Remote)
|
|
{
|
|
BundleRegistry.Instance.RunCodeGenForBundle(this);
|
|
}
|
|
if (needsInstall)
|
|
{
|
|
Install();
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Local means that the package exists on local disc but not inside the Unity project next to the npmdef
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public bool IsLocal => type == BundleType.Local;
|
|
|
|
/// <summary>
|
|
/// Embedded means that the package exists next to the npmdef in the Unity project. It is hidden with a ~
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public bool IsEmbedded => type == BundleType.Embedded;
|
|
|
|
public bool IsMutable()
|
|
{
|
|
var fp = System.IO.Path.GetFullPath(FilePath);
|
|
return !fp.Contains("Library\\PackageCache");
|
|
}
|
|
|
|
public bool IsValid()
|
|
{
|
|
return !string.IsNullOrEmpty(Name) && File.Exists(PackageFilePath);
|
|
}
|
|
|
|
public bool Validate()
|
|
{
|
|
if (type == BundleType.Remote)
|
|
{
|
|
return !string.IsNullOrWhiteSpace(packageName) && !string.IsNullOrWhiteSpace(packageVersion);
|
|
}
|
|
|
|
var dir = PackageDirectory;
|
|
if (Directory.Exists(dir))
|
|
{
|
|
if (!NeedlePackageConfig.Exists(dir))
|
|
NeedlePackageConfig.Create(dir);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// TODO: cache this to avoid file reads
|
|
[JsonIgnore]
|
|
public string PackageDirectory
|
|
{
|
|
get
|
|
{
|
|
switch (type)
|
|
{
|
|
case BundleType.Embedded:
|
|
return GetEmbeddedPath();
|
|
|
|
case BundleType.Local:
|
|
if (localPath != null && Directory.Exists(localPath))
|
|
{
|
|
if(Path.IsPathRooted(localPath)) return Path.GetFullPath(localPath);
|
|
// resolve full path relative to the npmdef file location
|
|
// we need to get the fullpath FIRST to resolve virtual paths when being installed via tgz
|
|
var dir = Path.GetDirectoryName(Path.GetFullPath(_filePath));
|
|
if (dir != null)
|
|
{
|
|
var path = Path.GetFullPath(Path.Combine(Path.GetFullPath(dir), localPath));
|
|
return path;
|
|
}
|
|
}
|
|
break;
|
|
|
|
// If the bundle is set to be remove we need to get the path from the export info
|
|
case BundleType.Remote:
|
|
var exp = ExportInfo.Get();
|
|
if (exp)
|
|
{
|
|
var webProjectPath = Path.GetFullPath(exp.GetProjectDirectory());
|
|
var installationPath = webProjectPath + "/node_modules/" + packageName;
|
|
return installationPath;
|
|
}
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string GetEmbeddedPath()
|
|
{
|
|
var path = Name;
|
|
if (path.EndsWith("package.json"))
|
|
path = Path.GetDirectoryName(path);
|
|
var dir = Path.GetDirectoryName(FilePath);
|
|
var fullDir = Path.GetFullPath(dir + "/" + path);
|
|
return fullDir;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Path to package json
|
|
/// </summary>
|
|
[JsonIgnore]
|
|
public string PackageFilePath => PackageDirectory + "/package.json";
|
|
|
|
#if UNITY_EDITOR
|
|
internal NpmDefObject LoadAsset()
|
|
{
|
|
return AssetDatabase.LoadAssetAtPath<NpmDefObject>(FilePath);
|
|
}
|
|
#endif
|
|
|
|
[JsonIgnore]
|
|
internal string FilePath
|
|
{
|
|
get => _filePath;
|
|
set
|
|
{
|
|
if (string.Equals(value, this._filePath, StringComparison.Ordinal)) return;
|
|
this._filePath = value;
|
|
codeGenDirectory = null;
|
|
}
|
|
}
|
|
|
|
[JsonIgnore, NonSerialized] private string _filePath;
|
|
|
|
public string FindPackageName()
|
|
{
|
|
var path = PackageDirectory + "/package.json";
|
|
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
|
{
|
|
return packageName;
|
|
}
|
|
var name = PackageUtils.GetPackageName(path);
|
|
return name;
|
|
}
|
|
|
|
public string FindPackageVersion()
|
|
{
|
|
var path = PackageDirectory + "/package.json";
|
|
if (string.IsNullOrEmpty(path) || !File.Exists(path))
|
|
{
|
|
return packageVersion;
|
|
}
|
|
if (PackageUtils.TryGetVersion(path, out var version))
|
|
return version;
|
|
return packageVersion;
|
|
}
|
|
|
|
private DirectoryInfo codeGenDirectory = null;
|
|
|
|
public string FindScriptGenDirectory()
|
|
{
|
|
if (codeGenDirectory == null)
|
|
{
|
|
var dir = new FileInfo(FilePath);
|
|
codeGenDirectory = new DirectoryInfo(dir.DirectoryName + "/" + System.IO.Path.GetFileNameWithoutExtension(FilePath) + ".codegen");
|
|
}
|
|
return codeGenDirectory.FullName;
|
|
}
|
|
|
|
public bool IsInstalled(string packageJsonPath)
|
|
{
|
|
if (packageJsonPath != null && File.Exists(packageJsonPath))
|
|
return PackageUtils.IsDependency(packageJsonPath, FindPackageName());
|
|
return false;
|
|
}
|
|
|
|
public bool Install(ExportInfo exportInfo = null)
|
|
{
|
|
var exp = exportInfo ? exportInfo : ExportInfo.Get();
|
|
if (!exp || !exp.Exists())
|
|
{
|
|
Debug.LogWarning("Web Project not found. Please create your Needle web project first.");
|
|
return false;
|
|
}
|
|
var path = PackageDirectory;
|
|
// For local packages we can change the installed path to the local package
|
|
if (type != BundleType.Remote && Directory.Exists(path))
|
|
{
|
|
var projectDirectory = exp.GetProjectDirectory();
|
|
if (PackageUtils.AddPackage(projectDirectory, path))
|
|
{
|
|
Debug.Log("<b>Added package</b> " + FilePath + " to " + exp.PackageJsonPath.AsLink());
|
|
TypesUtils.MarkDirty();
|
|
Actions.AddToWorkspace(projectDirectory, FindPackageName());
|
|
return true;
|
|
}
|
|
Debug.LogWarning("Installation failed: " + path);
|
|
}
|
|
// For remote packages we only update the dependency if it doesnt exist yet
|
|
else if (!string.IsNullOrWhiteSpace(packageName) && !string.IsNullOrWhiteSpace(packageVersion))
|
|
{
|
|
if (PackageUtils.TryReadDependencies(exp.PackageJsonPath, out var deps))
|
|
{
|
|
if (!deps.ContainsKey(packageName))
|
|
{
|
|
deps[packageName] = packageVersion;
|
|
if (PackageUtils.TryWriteDependencies(exp.PackageJsonPath, deps))
|
|
return true;
|
|
}
|
|
// If the package is already installed in the web project just do nothing
|
|
else return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Uninstall(ExportInfo exp = null)
|
|
{
|
|
exp = exp ? exp : ExportInfo.Get();
|
|
if (!exp) return;
|
|
var name = FindPackageName();
|
|
if (PackageUtils.TryReadDependencies(exp.PackageJsonPath, out var deps))
|
|
{
|
|
if (deps.ContainsKey(name))
|
|
{
|
|
deps.Remove(name);
|
|
if (PackageUtils.TryWriteDependencies(exp.PackageJsonPath, deps))
|
|
{
|
|
Actions.RemoveFromWorkspace(exp.GetProjectDirectory(), FindPackageName());
|
|
Debug.Log("<b>Removed package</b> " + name + " from " + exp.PackageJsonPath.AsLink());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public Task<bool> RunInstall()
|
|
{
|
|
return Actions.InstallBundleTask(this);
|
|
}
|
|
|
|
internal void Save(string path, bool refresh = true)
|
|
{
|
|
var json = JsonConvert.SerializeObject(this, Formatting.Indented);
|
|
File.WriteAllText(path, json);
|
|
BundleRegistry.Instance.MarkDirty();
|
|
if (refresh)
|
|
{
|
|
AssetDatabase.Refresh();
|
|
}
|
|
// AssetDatabase.ImportAsset(FilePath, ImportAssetOptions.ForceSynchronousImport);
|
|
}
|
|
|
|
internal void FindImports(List<ImportInfo> list, [CanBeNull] string projectDirectory, bool includeAbstract = false)
|
|
{
|
|
var packageDir = PackageDirectory;
|
|
if (!Directory.Exists(packageDir)) return;
|
|
var installed = projectDirectory == null || IsInstalled(projectDirectory + "/package.json");
|
|
var startCount = list.Count;
|
|
TypeScanner.FindTypes(packageDir, list, SearchOption.TopDirectoryOnly, includeAbstract);
|
|
RecursiveFindTypesIgnoringNodeModules(list, packageDir, includeAbstract);
|
|
for (var i = startCount; i < list.Count; i++)
|
|
list[i].IsInstalled = installed;
|
|
}
|
|
|
|
internal IEnumerable<string> EnumerateDirectories(bool skipNodeModule = true, int maxLevel = 2)
|
|
{
|
|
IEnumerable<string> EnumerateDir(DirectoryInfo currentDirectory, int currentLevel)
|
|
{
|
|
if (!currentDirectory.Exists) yield break;
|
|
if (skipNodeModule && currentDirectory.Name == "node_modules") yield break;
|
|
if(currentDirectory.Name.EndsWith("codegen")) yield break;
|
|
yield return currentDirectory.FullName;
|
|
if (currentLevel >= maxLevel) yield break;
|
|
var dirs = currentDirectory.GetDirectories();
|
|
foreach (var d in dirs)
|
|
{
|
|
foreach (var sub in EnumerateDir(d, currentLevel + 1))
|
|
yield return sub;
|
|
}
|
|
}
|
|
|
|
var dir = new DirectoryInfo(PackageDirectory);
|
|
return EnumerateDir(dir, 0);
|
|
}
|
|
|
|
private static void RecursiveFindTypesIgnoringNodeModules(List<ImportInfo> list, string currentDir, bool includeAbstract = false)
|
|
{
|
|
if (!Directory.Exists(currentDir)) return;
|
|
foreach (var dir in Directory.EnumerateDirectories(currentDir))
|
|
{
|
|
if (dir.EndsWith("node_modules")) continue;
|
|
TypeScanner.FindTypes(dir, list, SearchOption.TopDirectoryOnly, includeAbstract);
|
|
RecursiveFindTypesIgnoringNodeModules(list, dir, includeAbstract);
|
|
}
|
|
}
|
|
|
|
// private void FindCodeGenDirectory(ref DirectoryInfo dir)
|
|
// {
|
|
// if (dir?.Exists ?? false) return;
|
|
// var currentDirectory = System.IO.Path.GetDirectoryName(FilePath);
|
|
// Debug.Log(currentDirectory);
|
|
// var folders = new string[] { currentDirectory };
|
|
// var guids = AssetDatabase.FindAssets("t:" + nameof(AssemblyDefinitionAsset), folders);
|
|
// foreach (var guid in guids)
|
|
// {
|
|
// var path = AssetDatabase.GUIDToAssetPath(guid);
|
|
// var asset = AssetDatabase.LoadAssetAtPath<AssemblyDefinitionAsset>(path);
|
|
//
|
|
// }
|
|
// // while (currentDirectory != null)
|
|
// // {
|
|
// // foreach (var asmdefPath in Directory.EnumerateFiles(currentDirectory, "*.asmdef", SearchOption.TopDirectoryOnly))
|
|
// // {
|
|
// // // Compiler
|
|
// // }
|
|
// // }
|
|
// }
|
|
}
|
|
} |