Files
AR-Menu/Library/PackageCache/com.needle.engine-exporter@8c046140a1d9/ProjectBundle/Editor/Bundle.cs
2025-11-30 08:35:03 +02:00

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
// // }
// // }
// }
}
}