diff --git a/Nodejs/Product/Analysis/Analysis.csproj b/Nodejs/Product/Analysis/Analysis.csproj index 84a05dc5a..9bb0976f1 100644 --- a/Nodejs/Product/Analysis/Analysis.csproj +++ b/Nodejs/Product/Analysis/Analysis.csproj @@ -78,6 +78,7 @@ + @@ -107,6 +108,7 @@ + diff --git a/Nodejs/Product/Analysis/Analysis/AnalysisLimits.cs b/Nodejs/Product/Analysis/Analysis/AnalysisLimits.cs index e583b2dae..6d953de3b 100644 --- a/Nodejs/Product/Analysis/Analysis/AnalysisLimits.cs +++ b/Nodejs/Product/Analysis/Analysis/AnalysisLimits.cs @@ -157,23 +157,10 @@ public static AnalysisLimits MakeLowAnalysisLimits() { /// /// Checks whether relative path exceed the nested module limit. /// - /// Path to module file which has to be checked for depth limit. + /// Depth of module file which has to be checked for depth limit. /// True if path too deep in nesting tree; false overwise. - public bool IsPathExceedNestingLimit(string path) { - int nestedModulesCount = 0; - int startIndex = 0; - int index = path.IndexOf(AnalysisConstants.NodeModulesFolder, startIndex, StringComparison.OrdinalIgnoreCase); - while (index != -1) { - nestedModulesCount++; - if (nestedModulesCount > this.NestedModulesLimit){ - return true; - } - - startIndex = index + AnalysisConstants.NodeModulesFolder.Length; - index = path.IndexOf(AnalysisConstants.NodeModulesFolder, startIndex, StringComparison.OrdinalIgnoreCase); - } - - return false; + public bool IsPathExceedNestingLimit(int nestedModulesDepth) { + return nestedModulesDepth > NestedModulesLimit; } public override bool Equals(object obj) { diff --git a/Nodejs/Product/Analysis/Analysis/AnalysisSerializationSupportedTypeAttribute.cs b/Nodejs/Product/Analysis/Analysis/AnalysisSerializationSupportedTypeAttribute.cs index c5397b07f..6893d6147 100644 --- a/Nodejs/Product/Analysis/Analysis/AnalysisSerializationSupportedTypeAttribute.cs +++ b/Nodejs/Product/Analysis/Analysis/AnalysisSerializationSupportedTypeAttribute.cs @@ -96,6 +96,7 @@ [assembly: AnalysisSerializationSupportedType(typeof(FunctionAnalysisUnit))] [assembly: AnalysisSerializationSupportedType(typeof(CartesianProductFunctionAnalysisUnit))] [assembly: AnalysisSerializationSupportedType(typeof(CartesianProductFunctionAnalysisUnit.CartesianLocalVariable))] +[assembly: AnalysisSerializationSupportedType(typeof(RequireAnalysisUnit))] [assembly: AnalysisSerializationSupportedType(typeof(ReferenceDict))] [assembly: AnalysisSerializationSupportedType(typeof(ReferenceList))] [assembly: AnalysisSerializationSupportedType(typeof(DependentKeyValue))] diff --git a/Nodejs/Product/Analysis/Analysis/Analyzer/RequireAnalysisUnit.cs b/Nodejs/Product/Analysis/Analysis/Analyzer/RequireAnalysisUnit.cs new file mode 100644 index 000000000..dd3b2899d --- /dev/null +++ b/Nodejs/Product/Analysis/Analysis/Analyzer/RequireAnalysisUnit.cs @@ -0,0 +1,52 @@ +//*********************************************************// +// Copyright (c) Microsoft. All rights reserved. +// +// Apache 2.0 License +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. +// +//*********************************************************// + +using System.Diagnostics; +using System.Threading; + +namespace Microsoft.NodejsTools.Analysis.Analyzer { + class RequireAnalysisUnit : AnalysisUnit { + private string _dependency; + private ModuleTree _tree; + private ModuleTable _table; + + internal RequireAnalysisUnit(ModuleTree tree, ModuleTable table, ProjectEntry entry, string dependency) : base (entry.Tree, entry.EnvironmentRecord) { + _tree = tree; + _table = table; + _dependency = dependency; + } + + internal override void AnalyzeWorker(DDG ddg, CancellationToken cancel) { + ModuleTree module = _table.RequireModule(this, _dependency, _tree); + if (module == null) { + return; + } + + AddChildVisibilitiesExcludingNodeModules(module); + } + + private void AddChildVisibilitiesExcludingNodeModules(ModuleTree moduleTree) { + foreach (var childTree in moduleTree.GetChildrenExcludingNodeModules()) { + Debug.Assert(childTree.Name != AnalysisConstants.NodeModulesFolder); + if (childTree.ProjectEntry == null) { + AddChildVisibilitiesExcludingNodeModules(childTree); + } else { + _table.AddVisibility(_tree, childTree.ProjectEntry); + } + } + } + } +} \ No newline at end of file diff --git a/Nodejs/Product/Analysis/Analysis/JsAnalyzer.cs b/Nodejs/Product/Analysis/Analysis/JsAnalyzer.cs index 65660595f..c45144e58 100644 --- a/Nodejs/Product/Analysis/Analysis/JsAnalyzer.cs +++ b/Nodejs/Product/Analysis/Analysis/JsAnalyzer.cs @@ -129,7 +129,7 @@ public ProjectEntry AddModule(string filePath, IAnalysisCookie cookie = null) { return entry; } - public IAnalyzable AddPackageJson(string filePath, string entryPoint) { + public IAnalyzable AddPackageJson(string filePath, string entryPoint, List dependencies = null) { if (!Path.GetFileName(filePath).Equals("package.json", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("path must be to package.json file"); } @@ -140,10 +140,18 @@ public IAnalyzable AddPackageJson(string filePath, string entryPoint) { } var tree = Modules.GetModuleTree(Path.GetDirectoryName(filePath)); - - tree.DefaultPackage = entryPoint; - return new TreeUpdateAnalysis(tree); + tree.DefaultPackage = entryPoint; + + var requireAnalysisUnits = new List(); + if (dependencies != null) { + var projectEntry = new ProjectEntry(this, filePath, null); + requireAnalysisUnits.AddRange(dependencies.Select( + dependency => { return new RequireAnalysisUnit(tree, Modules, projectEntry, dependency); + })); + } + + return new TreeUpdateAnalysis(tree, requireAnalysisUnits); } /// @@ -154,14 +162,22 @@ public IAnalyzable AddPackageJson(string filePath, string entryPoint) { [Serializable] internal class TreeUpdateAnalysis : IAnalyzable { private readonly ModuleTree _tree; - public TreeUpdateAnalysis(ModuleTree tree) { + private readonly IEnumerable _requireAnalysisUnits; + + public TreeUpdateAnalysis(ModuleTree tree, IEnumerable requireAnalysisUnits = null) { _tree = tree; + _requireAnalysisUnits = requireAnalysisUnits; } public void Analyze(CancellationToken cancel) { if (_tree != null) { _tree.EnqueueDependents(); } + if (_requireAnalysisUnits != null) { + foreach (var unit in _requireAnalysisUnits) { + unit.AnalyzeWorker(null, cancel); + } + } } } diff --git a/Nodejs/Product/Analysis/Analysis/ModuleAnalysis.cs b/Nodejs/Product/Analysis/Analysis/ModuleAnalysis.cs index 906a25cf1..874477e02 100644 --- a/Nodejs/Product/Analysis/Analysis/ModuleAnalysis.cs +++ b/Nodejs/Product/Analysis/Analysis/ModuleAnalysis.cs @@ -699,7 +699,7 @@ internal IEnumerable GetModules() { do { ModuleTree nodeModules; if (parentMT.Children.TryGetValue(AnalysisConstants.NodeModulesFolder, out nodeModules)) { - foreach (var child in GetChildrenExcludingNodeModules(nodeModules)) { + foreach (var child in nodeModules.GetChildrenExcludingNodeModules()) { var module = ModuleTable.ResolveModule(child.Parent, child.Name); if (module != null) { res.Add(MakeProjectMemberResult(module.Name)); @@ -709,7 +709,7 @@ internal IEnumerable GetModules() { parentMT = parentMT.Parent; } while (parentMT != null); - foreach (var sibling in GetChildrenExcludingNodeModules(moduleTree.Parent)) { + foreach (var sibling in moduleTree.Parent.GetChildrenExcludingNodeModules()) { if (sibling == moduleTree) { //Don't let us require ourself continue; @@ -731,19 +731,8 @@ private MemberResult MakeProjectMemberResult(string module) { JsMemberType.Module); } - private IEnumerable GetChildrenExcludingNodeModules(ModuleTree moduleTree) { - if (moduleTree == null) { - return Enumerable.Empty(); - } - //Children.Values returns an IEnumerable - // The process of resolving modules can lead us to add entries into the underlying array - // doing so results in exceptions b/c the array has changed under the enumerable - // To avoid this, we call .ToArray() to create a copy of the array locally which we then Enumerate - return moduleTree.Children.Values.ToArray().Where(mod => !String.Equals(mod.Name, AnalysisConstants.NodeModulesFolder, StringComparison.OrdinalIgnoreCase)); - } - private void GetChildModules(List res, ModuleTree moduleTree, string projectRelative) { - foreach (var child in GetChildrenExcludingNodeModules(moduleTree)) { + foreach (var child in moduleTree.GetChildrenExcludingNodeModules()) { Debug.Assert(child.Name != AnalysisConstants.NodeModulesFolder); if (child.ProjectEntry == null) { GetChildModules(res, child, projectRelative + child.Name + "/"); diff --git a/Nodejs/Product/Analysis/Analysis/ModuleTable.cs b/Nodejs/Product/Analysis/Analysis/ModuleTable.cs index 84232dcb4..ed4f8b79a 100644 --- a/Nodejs/Product/Analysis/Analysis/ModuleTable.cs +++ b/Nodejs/Product/Analysis/Analysis/ModuleTable.cs @@ -115,7 +115,7 @@ public void AddModule(string name, ProjectEntry value) { tree.ProjectEntry = value; // Update visibility.. - AddVisibility(tree, value, true); + AddVisibility(tree, value); value._enqueueModuleDependencies = true; @@ -141,7 +141,7 @@ public void AddModule(string name, ProjectEntry value) { // We share hashsets of visible nodes. They're stored in the ModuleTree and when a // new module is added we assign it's _visibleEntries field to the one shared by all // of it's peers. We then update the relevant entries with the new values. - private void AddVisibility(ModuleTree tree, ProjectEntry newModule, bool recurse) { + internal void AddVisibility(ModuleTree tree, ProjectEntry newModule) { // My peers can see my assignments/I can see my peers assignments. Update // ourselves and our peers so we can see each others writes. var curTree = tree; @@ -202,28 +202,29 @@ public IAnalysisSet RequireModule(Node node, AnalysisUnit unit, string moduleNam } private IAnalysisSet RequireModule(Node node, AnalysisUnit unit, string moduleName, ModuleTree relativeTo) { + ModuleTree tree = RequireModule(unit, moduleName, relativeTo); + if (node != null) { + return GetExports(node, unit, tree); + } + return AnalysisSet.Empty; + } + + internal ModuleTree RequireModule(AnalysisUnit unit, string moduleName, ModuleTree relativeTo) { + ModuleTree curTree = null; if (moduleName.StartsWith("./") || moduleName.StartsWith("../")) { // search relative to our declaring module. - return GetExports( - node, - unit, - ResolveModule(relativeTo, moduleName, unit) - ); + curTree = ResolveModule(relativeTo, moduleName, unit); } else { // must be in node_modules, search in the current directory // and up through our parents do { var nodeModules = relativeTo.GetChild(AnalysisConstants.NodeModulesFolder, unit); - var curTree = ResolveModule(nodeModules, moduleName, unit); - - if (curTree != null) { - return GetExports(node, unit, curTree); - } + curTree = ResolveModule(nodeModules, moduleName, unit); relativeTo = relativeTo.Parent; - } while (relativeTo != null); + } while (relativeTo != null && curTree == null); } - return AnalysisSet.Empty; + return curTree; } public static ModuleTree ResolveModule(ModuleTree parentTree, string relativeName) { diff --git a/Nodejs/Product/Analysis/Analysis/ModuleTreeExtensions.cs b/Nodejs/Product/Analysis/Analysis/ModuleTreeExtensions.cs new file mode 100644 index 000000000..d08542c45 --- /dev/null +++ b/Nodejs/Product/Analysis/Analysis/ModuleTreeExtensions.cs @@ -0,0 +1,34 @@ +//*********************************************************// +// Copyright (c) Microsoft. All rights reserved. +// +// Apache 2.0 License +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. +// +//*********************************************************// + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.NodejsTools.Analysis { + internal static class ModuleTreeExtensions { + internal static IEnumerable GetChildrenExcludingNodeModules(this ModuleTree moduleTree) { + if (moduleTree == null) { + return Enumerable.Empty(); + } + // Children.Values returns an IEnumerable + // The process of resolving modules can lead us to add entries into the underlying array + // doing so results in exceptions b/c the array has changed under the enumerable + // To avoid this, we call .ToArray() to create a copy of the array locally which we then Enumerate + return moduleTree.Children.Values.ToArray().Where(mod => !String.Equals(mod.Name, AnalysisConstants.NodeModulesFolder, StringComparison.OrdinalIgnoreCase)); + } + } +} diff --git a/Nodejs/Product/Nodejs/Extensions.cs b/Nodejs/Product/Nodejs/Extensions.cs index a6513ce35..a5b6cdfb9 100644 --- a/Nodejs/Product/Nodejs/Extensions.cs +++ b/Nodejs/Product/Nodejs/Extensions.cs @@ -112,7 +112,7 @@ out project internal static IComponentModel GetComponentModel(this IServiceProvider serviceProvider) { return (IComponentModel)serviceProvider.GetService(typeof(SComponentModel)); } - + internal static string GetFilePath(this ITextBuffer textBuffer) { ITextDocument textDocument; if (textBuffer.Properties.TryGetProperty(typeof(ITextDocument), out textDocument)) { @@ -290,7 +290,7 @@ internal static void GotoSource(this LocationInfo location) { location.FilePath, location.Line - 1, location.Column - 1 - ); + ); } internal static SnapshotPoint? GetCaretPosition(this ITextView view) { @@ -435,4 +435,4 @@ internal static ITrackingSpan GetApplicableSpan(this ITextSnapshot snapshot, int return null; } } -} +} \ No newline at end of file diff --git a/Nodejs/Product/Nodejs/Intellisense/VsProjectAnalyzer.cs b/Nodejs/Product/Nodejs/Intellisense/VsProjectAnalyzer.cs index 33eed2a52..dd7109623 100644 --- a/Nodejs/Product/Nodejs/Intellisense/VsProjectAnalyzer.cs +++ b/Nodejs/Product/Nodejs/Intellisense/VsProjectAnalyzer.cs @@ -374,16 +374,30 @@ public void AddPackageJson(string packageJsonPath) { object mainFile; if (json != null && json.TryGetValue("main", out mainFile) && mainFile is string) { - AddPackageJson(packageJsonPath, (string)mainFile); + List dependencyList = GetDependencyListFromJson(json, "dependencies", "devDependencies", "optionalDependencies"); + AddPackageJson(packageJsonPath, (string)mainFile, dependencyList); } } - } - - public void AddPackageJson(string path, string mainFile) { + } + + private static List GetDependencyListFromJson(Dictionary json, params string[] dependencyTypes) { + var allDependencies = new List(); + foreach (var type in dependencyTypes) { + object dependencies; + json.TryGetValue(type, out dependencies); + var dep = dependencies as Dictionary; + if (dep != null) { + allDependencies.AddRange(dep.Keys.ToList()); + } + } + return allDependencies; + } + + public void AddPackageJson(string path, string mainFile, List dependencies) { if (!_fullyLoaded) { lock (_loadingDeltas) { if (!_fullyLoaded) { - _loadingDeltas.Add(() => AddPackageJson(path, mainFile)); + _loadingDeltas.Add(() => AddPackageJson(path, mainFile, dependencies)); return; } } @@ -391,7 +405,7 @@ public void AddPackageJson(string path, string mainFile) { if (ShouldEnqueue()) { _analysisQueue.Enqueue( - _jsAnalyzer.AddPackageJson(path, mainFile), + _jsAnalyzer.AddPackageJson(path, mainFile, dependencies), AnalysisPriority.Normal ); } diff --git a/Nodejs/Product/Nodejs/NodejsConstants.cs b/Nodejs/Product/Nodejs/NodejsConstants.cs index 4244c0a74..505cb310b 100644 --- a/Nodejs/Product/Nodejs/NodejsConstants.cs +++ b/Nodejs/Product/Nodejs/NodejsConstants.cs @@ -39,6 +39,7 @@ internal class NodejsConstants { internal const string StartWebBrowser = "StartWebBrowser"; internal const string NodeModulesFolder = "node_modules"; + internal const string NodeModulesStagingFolder = "node_modules\\.staging\\"; internal const string AnalysisIgnoredDirectories = "AnalysisIgnoredDirectories"; internal const string AnalysisMaxFileSize = "AnalysisMaxFileSize"; internal const string DefaultIntellisenseCompletionCommittedBy = "{}[]().,:;+-*/%&|^!~=<>?@#'\"\\"; diff --git a/Nodejs/Product/Nodejs/Project/NodejsFileNode.cs b/Nodejs/Product/Nodejs/Project/NodejsFileNode.cs index 6d60dd405..bd4de1066 100644 --- a/Nodejs/Product/Nodejs/Project/NodejsFileNode.cs +++ b/Nodejs/Product/Nodejs/Project/NodejsFileNode.cs @@ -15,10 +15,7 @@ //*********************************************************// using System; -using System.Diagnostics; using System.IO; -using Microsoft.VisualStudio; -using Microsoft.VisualStudioTools; using Microsoft.VisualStudioTools.Project; #if DEV14_OR_LATER using Microsoft.VisualStudio.Imaging.Interop; @@ -36,11 +33,18 @@ public NodejsFileNode(NodejsProjectNode root, ProjectElement e) #if FALSE CreateWatcher(Url); #endif - if (ShouldAnalyze) { - root.Analyzer.AnalyzeFile(Url, !IsNonMemberItem); - root._requireCompletionCache.Clear(); + if (Url.Contains(AnalysisConstants.NodeModulesFolder)) { + root.DelayedAnalysisQueue.Enqueue(this); + } else { + Analyze(); + } + } + + internal void Analyze() { + if (ProjectMgr.Analyzer != null && ShouldAnalyze) { + ProjectMgr.Analyzer.AnalyzeFile(Url, !IsNonMemberItem); + ProjectMgr._requireCompletionCache.Clear(); } - ItemNode.ItemTypeChanged += ItemNode_ItemTypeChanged; } @@ -48,8 +52,9 @@ internal bool ShouldAnalyze { get { // We analyze if we are a member item or the file is included // Also, it should either be marked as compile or not have an item type name (value is null for node_modules - return (!IsNonMemberItem || ProjectMgr.IncludeNodejsFile(this)) - && (ItemNode.ItemTypeName == ProjectFileConstants.Compile || string.IsNullOrEmpty(ItemNode.ItemTypeName)); + return !ProjectMgr.DelayedAnalysisQueue.Contains(this) && + (!IsNonMemberItem || ProjectMgr.IncludeNodejsFile(this)) && + (ItemNode.ItemTypeName == ProjectFileConstants.Compile || string.IsNullOrEmpty(ItemNode.ItemTypeName)); } } diff --git a/Nodejs/Product/Nodejs/Project/NodejsProjectNode.cs b/Nodejs/Product/Nodejs/Project/NodejsProjectNode.cs index ddab847b6..30af41fb4 100644 --- a/Nodejs/Product/Nodejs/Project/NodejsProjectNode.cs +++ b/Nodejs/Product/Nodejs/Project/NodejsProjectNode.cs @@ -22,6 +22,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.NodejsTools.Intellisense; using Microsoft.NodejsTools.Npm; @@ -43,12 +44,19 @@ class NodejsProjectNode : CommonProjectNode, VsWebSite.VSWebSite, INodePackageMo private VsProjectAnalyzer _analyzer; private readonly HashSet _warningFiles = new HashSet(); private readonly HashSet _errorFiles = new HashSet(); - private string[] _analysisIgnoredDirs = new string[0]; + private string[] _analysisIgnoredDirs = new string[1] { NodejsConstants.NodeModulesStagingFolder }; private int _maxFileSize = 1024 * 512; internal readonly RequireCompletionCache _requireCompletionCache = new RequireCompletionCache(); private string _intermediateOutputPath; private readonly Dictionary _imageIndexFromNameDictionary = new Dictionary(); + // We delay analysis until things calm down in the node_modules folder. + internal Queue DelayedAnalysisQueue = new Queue(); + private FileWatcher _nodeModulesWatcher; + private readonly string[] _watcherExtensions = { ".js", ".json" }; + private object _idleNodeModulesLock = new object(); + private volatile bool _isIdleNodeModules = false; + private Timer _idleNodeModulesTimer; public NodejsProjectNode(NodejsProjectPackage package) : base( @@ -58,8 +66,7 @@ public NodejsProjectNode(NodejsProjectPackage package) #else Utilities.GetImageList(typeof(NodejsProjectNode).Assembly.GetManifestResourceStream("Microsoft.NodejsTools.Resources.Icons.NodejsImageList.bmp")) #endif - ) - { + ) { Type projectNodePropsType = typeof(NodejsProjectNodeProperties); AddCATIDMapping(projectNodePropsType, projectNodePropsType.GUID); #pragma warning disable 0612 @@ -74,6 +81,73 @@ public VsProjectAnalyzer Analyzer { } } + private void CreateIdleNodeModulesWatcher() { + try { + _idleNodeModulesTimer = new Timer(OnIdleNodeModules); + + // This handles the case where there are multiple node_modules folders in a project. + _nodeModulesWatcher = new FileWatcher(ProjectHome) { + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, + EnableRaisingEvents = true + }; + + _nodeModulesWatcher.Changed += OnNodeModulesWatcherChanged; + _nodeModulesWatcher.Created += OnNodeModulesWatcherChanged; + _nodeModulesWatcher.Deleted += OnNodeModulesWatcherChanged; + + RestartFileSystemWatcherTimer(); + } catch (Exception ex) { + if (_nodeModulesWatcher != null) { + _nodeModulesWatcher.Dispose(); + } + + if (ex is IOException || ex is ArgumentException) { + Debug.WriteLine("Error starting FileWatcher:\r\n{0}", ex); + } else { + throw; + } + } + } + + private void OnIdleNodeModules(object state) { + lock (_idleNodeModulesLock) { + _isIdleNodeModules = true; + } + + while (DelayedAnalysisQueue.Count > 0) { + lock (_idleNodeModulesLock) { + if (!_isIdleNodeModules) { + return; + } + } + var fileNode = DelayedAnalysisQueue.Dequeue(); + fileNode.Analyze(); + } + } + + private void OnNodeModulesWatcherChanged(object sender, FileSystemEventArgs e) { + try { + var extension = Path.GetExtension(e.FullPath); + if (e.FullPath.Contains(NodejsConstants.NodeModulesFolder) && _watcherExtensions.Any(extension.Equals)) { + RestartFileSystemWatcherTimer(); + } + } catch (ArgumentException) { + // Occurs for invalid characters in the filepath. Don't bother restarting the idle timer. + } + } + + private void RestartFileSystemWatcherTimer() { + lock (_idleNodeModulesLock) { + _isIdleNodeModules = false; + } + + // The cooldown time here is longer than the cooldown time we use in NpmController. + // This gives the Npm component ample time to build up the npm node tree, + // so that we can query it later for perf optimizations. + _idleNodeModulesTimer.Change(3000, Timeout.Infinite); + } + private static string[] _excludedAvailableItems = new[] { "ApplicationDefinition", "Page", @@ -209,8 +283,8 @@ internal static bool IsNodejsFile(string strFileName) { } internal override string GetItemType(string filename) { - string absFileName = - Path.IsPathRooted(filename) ? + string absFileName = + Path.IsPathRooted(filename) ? filename : Path.Combine(this.ProjectHome, filename); @@ -277,7 +351,8 @@ public override CommonFileNode CreateCodeFileNode(ProjectElement item) { public override CommonFileNode CreateNonCodeFileNode(ProjectElement item) { string fileName = item.Url; - if (Path.GetFileName(fileName).Equals(NodejsConstants.PackageJsonFile, StringComparison.OrdinalIgnoreCase)) { + if (Path.GetFileName(fileName).Equals(NodejsConstants.PackageJsonFile, StringComparison.OrdinalIgnoreCase) && + !fileName.Contains(NodejsConstants.NodeModulesStagingFolder)) { return new PackageJsonFileNode(this, item); } @@ -336,7 +411,7 @@ public override IProjectLauncher GetLauncher() { protected override NodeProperties CreatePropertiesObject() { return new NodejsProjectNodeProperties(this); } - + public override int GetPropertyValue(string propertyName, string configName, uint storage, out string propertyValue) { propertyValue = null; switch (propertyName) { @@ -402,9 +477,7 @@ protected override void Reload() { var ignoredPaths = GetProjectProperty(NodejsConstants.AnalysisIgnoredDirectories); if (!string.IsNullOrWhiteSpace(ignoredPaths)) { - _analysisIgnoredDirs = ignoredPaths.Split(';').Select(x => '\\' + x + '\\').ToArray(); - } else { - _analysisIgnoredDirs = new string[0]; + _analysisIgnoredDirs.Append(ignoredPaths.Split(';').Select(x => '\\' + x + '\\').ToArray()); } var maxFileSizeProp = GetProjectProperty(NodejsConstants.AnalysisMaxFileSize); @@ -577,9 +650,13 @@ internal bool IncludeNodejsFile(NodejsFileNode fileNode) { return false; } - var relativeFile = CommonUtils.GetRelativeFilePath(this.FullPathToChildren, fileNode.Url); - if (this._analyzer != null && this._analyzer.Project != null - && this._analyzer.Project.Limits.IsPathExceedNestingLimit(relativeFile)) { + int nestedModulesDepth = 0; + if (ModulesNode.NpmController.RootPackage != null && ModulesNode.NpmController.RootPackage.Modules != null) { + nestedModulesDepth = ModulesNode.NpmController.RootPackage.Modules.GetDepth(fileNode.Url); + } + + if (_analyzer != null && _analyzer.Project != null && + _analyzer.Project.Limits.IsPathExceedNestingLimit(nestedModulesDepth)) { return false; } @@ -606,10 +683,11 @@ protected internal override void ProcessReferences() { if (null == ModulesNode) { ModulesNode = new NodeModulesNode(this); AddChild(ModulesNode); + CreateIdleNodeModulesWatcher(); } } -#region VSWebSite Members + #region VSWebSite Members // This interface is just implemented so we don't get normal profiling which // doesn't work with our projects anyway. @@ -676,7 +754,7 @@ public VsWebSite.WebServices WebServices { get { throw new NotImplementedException(); } } -#endregion + #endregion Task INodePackageModulesCommands.InstallMissingModulesAsync() { //Fire off the command to update the missing modules @@ -792,7 +870,7 @@ public async Task CheckForLongPaths(string npmArguments = null) { } }; - recheck: + recheck: var longPaths = await Task.Factory.StartNew(() => GetLongSubPaths(ProjectHome) diff --git a/Nodejs/Product/Npm/INodeModules.cs b/Nodejs/Product/Npm/INodeModules.cs index 4055a74d8..2e6dfccb7 100644 --- a/Nodejs/Product/Npm/INodeModules.cs +++ b/Nodejs/Product/Npm/INodeModules.cs @@ -23,5 +23,6 @@ public interface INodeModules : IEnumerable { IPackage this[string name] { get; } bool Contains(string name); bool HasMissingModules { get; } + int GetDepth(string filepath); } } \ No newline at end of file diff --git a/Nodejs/Product/Npm/IPackageJson.cs b/Nodejs/Product/Npm/IPackageJson.cs index e43a471fb..ac3f66e00 100644 --- a/Nodejs/Product/Npm/IPackageJson.cs +++ b/Nodejs/Product/Npm/IPackageJson.cs @@ -34,5 +34,6 @@ public interface IPackageJson { IBundledDependencies BundledDependencies { get; } IDependencies OptionalDependencies { get; } IDependencies AllDependencies { get; } + IEnumerable RequiredBy { get; } } } \ No newline at end of file diff --git a/Nodejs/Product/Npm/Npm.csproj b/Nodejs/Product/Npm/Npm.csproj index 710d84857..4b0a991b7 100644 --- a/Nodejs/Product/Npm/Npm.csproj +++ b/Nodejs/Product/Npm/Npm.csproj @@ -90,6 +90,9 @@ CommonUtils.cs + + NodejsConstants.cs + diff --git a/Nodejs/Product/Npm/SPI/AbstractNodeModules.cs b/Nodejs/Product/Npm/SPI/AbstractNodeModules.cs index 74fbe3c11..f1f07452f 100644 --- a/Nodejs/Product/Npm/SPI/AbstractNodeModules.cs +++ b/Nodejs/Product/Npm/SPI/AbstractNodeModules.cs @@ -14,6 +14,7 @@ // //*********************************************************// +using System; using System.Collections; using System.Collections.Generic; @@ -23,8 +24,10 @@ internal abstract class AbstractNodeModules : INodeModules { private readonly IDictionary _packagesByName = new Dictionary(); protected virtual void AddModule(IPackage package) { - _packagesSorted.Add(package); - _packagesByName[package.Name] = package; + if (!_packagesSorted.Contains(package) && package.Name != null) { + _packagesSorted.Add(package); + _packagesByName[package.Name] = package; + } } public int Count { @@ -65,5 +68,7 @@ public IEnumerator GetEnumerator() { IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + public abstract int GetDepth(string filepath); } } \ No newline at end of file diff --git a/Nodejs/Product/Npm/SPI/NodeModules.cs b/Nodejs/Product/Npm/SPI/NodeModules.cs index 26ae8fa70..ea60e370c 100644 --- a/Nodejs/Product/Npm/SPI/NodeModules.cs +++ b/Nodejs/Product/Npm/SPI/NodeModules.cs @@ -14,47 +14,146 @@ // //*********************************************************// +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; namespace Microsoft.NodejsTools.Npm.SPI { internal class NodeModules : AbstractNodeModules { - public NodeModules(IRootPackage parent, bool showMissingDevOptionalSubPackages) { - var modulesBase = Path.Combine(parent.Path, "node_modules"); - if (modulesBase.Length < NativeMethods.MAX_FOLDER_PATH && Directory.Exists(modulesBase)) { - var bin = string.Format("{0}.bin", Path.DirectorySeparatorChar); - foreach (var moduleDir in Directory.EnumerateDirectories(modulesBase)) { - if (moduleDir.Length < NativeMethods.MAX_FOLDER_PATH && !moduleDir.EndsWith(bin)) { - AddModule(new Package(parent, moduleDir, showMissingDevOptionalSubPackages)); - } + private Dictionary _allModules; + private readonly string[] _ignoredDirectories = { @"\.bin", @"\.staging" }; + + public NodeModules(IRootPackage parent, bool showMissingDevOptionalSubPackages, Dictionary allModulesToDepth = null, int depth = 0) { + var modulesBase = Path.Combine(parent.Path, NodejsConstants.NodeModulesFolder); + + _allModules = allModulesToDepth ?? new Dictionary(); + + // This is the first time NodeModules is being created. + // Iterate through directories to add everything that's known to be top-level. + if (depth == 0) { + Debug.Assert(_allModules.Count == 0, "Depth is 0, but top-level modules have already been added."); + + IEnumerable topLevelDirectories = Enumerable.Empty(); + try { + topLevelDirectories = Directory.EnumerateDirectories(modulesBase); + } catch (IOException) { + // We want to handle DirectoryNotFound, DriveNotFound, PathTooLong + } catch (UnauthorizedAccessException) { } - } - var parentPackageJson = parent.PackageJson; - if (null != parentPackageJson) { - foreach (var dependency in parentPackageJson.AllDependencies) { - Package module = null; - if (!Contains(dependency.Name)) { - var dependencyPath = Path.Combine(modulesBase, dependency.Name); - if (dependencyPath.Length < NativeMethods.MAX_FOLDER_PATH) { - module = new Package( - parent, - dependencyPath, - showMissingDevOptionalSubPackages); - if (parent as IPackage == null || !module.IsMissing || showMissingDevOptionalSubPackages) { - AddModule(module); + // Go through every directory in node_modules, and see if it's required as a top-level dependency + foreach (var moduleDir in topLevelDirectories) { + if (moduleDir.Length < NativeMethods.MAX_FOLDER_PATH && !_ignoredDirectories.Any(toIgnore => moduleDir.EndsWith(toIgnore))) { + var packageJson = PackageJsonFactory.Create(new DirectoryPackageJsonSource(moduleDir)); + + if (packageJson != null) { + if (packageJson.RequiredBy.Count() > 0) { + // All dependencies in npm v3 will have at least one element present in _requiredBy. + // _requiredBy dependencies that begin with hash characters represent top-level dependencies + foreach (var requiredBy in packageJson.RequiredBy) { + if (requiredBy.StartsWith("#") || requiredBy == "/") { + AddTopLevelModule(parent, showMissingDevOptionalSubPackages, moduleDir, depth); + break; + } + } + } else { + // This dependency is a top-level dependency not added by npm v3 + AddTopLevelModule(parent, showMissingDevOptionalSubPackages, moduleDir, depth); } } - } else { - module = this[dependency.Name] as Package; } + } + } - if (null != module) { - module.RequestedVersionRange = dependency.VersionRangeText; - } + if (modulesBase.Length < NativeMethods.MAX_FOLDER_PATH && parent.HasPackageJson) { + // Iterate through all dependencies in package.json + foreach (var dependency in parent.PackageJson.AllDependencies) { + var moduleDir = modulesBase; + + // try to find folder by recursing up tree + do { + moduleDir = Path.Combine(moduleDir, dependency.Name); + if (AddModuleIfNotExists(parent, moduleDir, showMissingDevOptionalSubPackages, depth, dependency)) { + break; + } + + var parentNodeModulesIndex = moduleDir.LastIndexOf(NodejsConstants.NodeModulesFolder, Math.Max(0, moduleDir.Length - NodejsConstants.NodeModulesFolder.Length - dependency.Name.Length - 1)); + moduleDir = moduleDir.Substring(0, parentNodeModulesIndex + NodejsConstants.NodeModulesFolder.Length); + } while (moduleDir.Contains(NodejsConstants.NodeModulesFolder)); } } _packagesSorted.Sort(new PackageComparer()); } + + private void AddTopLevelModule(IRootPackage parent, bool showMissingDevOptionalSubPackages, string moduleDir, int depth) { + Debug.Assert(depth == 0, "Depth should be 0 when adding a top level dependency"); + AddModuleIfNotExists(parent, moduleDir, showMissingDevOptionalSubPackages, depth); + } + + private bool AddModuleIfNotExists(IRootPackage parent, string moduleDir, bool showMissingDevOptionalSubPackages, int depth, IDependency dependency = null) { + depth++; + + ModuleInfo moduleInfo; + _allModules.TryGetValue(moduleDir, out moduleInfo); + + if (moduleInfo != null) { + if (moduleInfo.Depth > depth) { + moduleInfo.Depth = depth; + } + + if (dependency != null) { + var existingPackage = this[dependency.Name] as Package; + if (existingPackage != null) { + existingPackage.RequestedVersionRange = dependency.VersionRangeText; + } + } + } else if (Directory.Exists(moduleDir)) { + moduleInfo = new ModuleInfo(depth); + _allModules.Add(moduleDir, moduleInfo); + } else { + // The module directory wasn't found. + return false; + } + + if (moduleInfo.RequiredBy.Contains(parent.Name)) { + return true; + } + + moduleInfo.RequiredBy.Add(parent.Name); + var package = new Package(parent, moduleDir, showMissingDevOptionalSubPackages, _allModules, depth); + if (dependency != null) { + package.RequestedVersionRange = dependency.VersionRangeText; + } + AddModule(package); + + return true; + } + + public override int GetDepth(string filepath) { + var lastNodeModules = filepath.LastIndexOf(NodejsConstants.NodeModulesFolder + "\\"); + var directoryToSearch = filepath.IndexOf("\\", lastNodeModules + NodejsConstants.NodeModulesFolder.Length + 1); + var directorySubString = directoryToSearch == -1 ? filepath : filepath.Substring(0, directoryToSearch); + + ModuleInfo value = null; + _allModules.TryGetValue(directorySubString, out value); + + var depth = value != null ? value.Depth : 0; + Debug.WriteLine("Module Depth: {0} [{1}]", filepath, depth); + + return depth; + } + } + + internal class ModuleInfo { + public int Depth { get; set; } + public IList RequiredBy { get; set; } + + internal ModuleInfo(int depth) { + Depth = depth; + RequiredBy = new List(); + } } } \ No newline at end of file diff --git a/Nodejs/Product/Npm/SPI/NodeModulesProxy.cs b/Nodejs/Product/Npm/SPI/NodeModulesProxy.cs index 1941c7fbb..052fa0ebc 100644 --- a/Nodejs/Product/Npm/SPI/NodeModulesProxy.cs +++ b/Nodejs/Product/Npm/SPI/NodeModulesProxy.cs @@ -14,10 +14,16 @@ // //*********************************************************// +using System; + namespace Microsoft.NodejsTools.Npm.SPI { internal class NodeModulesProxy : AbstractNodeModules { public new void AddModule(IPackage package) { base.AddModule(package); } + + public override int GetDepth(string filepath) { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/Nodejs/Product/Npm/SPI/NpmController.cs b/Nodejs/Product/Npm/SPI/NpmController.cs index 0eba6ab2e..0a96a7fe2 100644 --- a/Nodejs/Product/Npm/SPI/NpmController.cs +++ b/Nodejs/Product/Npm/SPI/NpmController.cs @@ -277,7 +277,7 @@ private void Watcher_Modified(object sender, FileSystemEventArgs e) { // Check that the file is either a package.json file, or exists in the node_modules directory // This allows us to properly detect both installed and uninstalled/linked packages (where we don't receive an event for package.json) - if (path.EndsWith("package.json", StringComparison.OrdinalIgnoreCase) || path.IndexOf("node_modules", StringComparison.OrdinalIgnoreCase) != -1) { + if (path.EndsWith("package.json", StringComparison.OrdinalIgnoreCase) || path.IndexOf(NodejsConstants.NodeModulesFolder, StringComparison.OrdinalIgnoreCase) != -1) { RestartFileSystemWatcherTimer(); } @@ -291,6 +291,7 @@ private void RestartFileSystemWatcherTimer() { _fileSystemWatcherTimer.Dispose(); } + // Be sure to update the FileWatcher in NodejsProjectNode if the dueTime value changes. _fileSystemWatcherTimer = new Timer(o => UpdateModulesFromTimer(), null, 1000, Timeout.Infinite); } } diff --git a/Nodejs/Product/Npm/SPI/Package.cs b/Nodejs/Product/Npm/SPI/Package.cs index 2a420d250..5280fa858 100644 --- a/Nodejs/Product/Npm/SPI/Package.cs +++ b/Nodejs/Product/Npm/SPI/Package.cs @@ -26,8 +26,10 @@ internal class Package : RootPackage, IPackage { public Package( IRootPackage parent, string fullPathToRootDirectory, - bool showMissingDevOptionalSubPackages) - : base(fullPathToRootDirectory, showMissingDevOptionalSubPackages) { + bool showMissingDevOptionalSubPackages, + Dictionary allModules = null, + int depth = 0) + : base(fullPathToRootDirectory, showMissingDevOptionalSubPackages, allModules, depth) { _parent = parent; } diff --git a/Nodejs/Product/Npm/SPI/PackageJson.cs b/Nodejs/Product/Npm/SPI/PackageJson.cs index 5d8bf232a..a80f62f02 100644 --- a/Nodejs/Product/Npm/SPI/PackageJson.cs +++ b/Nodejs/Product/Npm/SPI/PackageJson.cs @@ -14,6 +14,7 @@ // //*********************************************************// +using System.Linq; using System.Collections.Generic; using Microsoft.CSharp.RuntimeBinder; using Newtonsoft.Json.Linq; @@ -37,6 +38,7 @@ public PackageJson(dynamic package) { InitBundledDependencies(); InitOptionalDependencies(); InitAllDependencies(); + InitRequiredBy(); } private void WrapRuntimeBinderExceptionAndRethrow( @@ -153,6 +155,17 @@ private void InitAllDependencies() { } } + private void InitRequiredBy() { + try { + RequiredBy = (_package["_requiredBy"] as IEnumerable ?? Enumerable.Empty()).Values(); + } catch (RuntimeBinderException rbe) { + System.Diagnostics.Debug.WriteLine(rbe); + WrapRuntimeBinderExceptionAndRethrow( + "required by", + rbe); + } + } + public string Name { get { return null == _package.name ? null : _package.name.ToString(); } } @@ -213,5 +226,6 @@ public IBugs Bugs { public IBundledDependencies BundledDependencies { get; private set; } public IDependencies OptionalDependencies { get; private set; } public IDependencies AllDependencies { get; private set; } + public IEnumerable RequiredBy { get; private set; } } } \ No newline at end of file diff --git a/Nodejs/Product/Npm/SPI/RootPackage.cs b/Nodejs/Product/Npm/SPI/RootPackage.cs index efcfe792a..af9b3bbf0 100644 --- a/Nodejs/Product/Npm/SPI/RootPackage.cs +++ b/Nodejs/Product/Npm/SPI/RootPackage.cs @@ -23,7 +23,9 @@ namespace Microsoft.NodejsTools.Npm.SPI { internal class RootPackage : IRootPackage { public RootPackage( string fullPathToRootDirectory, - bool showMissingDevOptionalSubPackages) { + bool showMissingDevOptionalSubPackages, + Dictionary allModules = null, + int depth = 0) { Path = fullPathToRootDirectory; var packageJsonFile = System.IO.Path.Combine(fullPathToRootDirectory, "package.json"); try { @@ -43,7 +45,7 @@ public RootPackage( } try { - Modules = new NodeModules(this, showMissingDevOptionalSubPackages); + Modules = new NodeModules(this, showMissingDevOptionalSubPackages, allModules, depth); } catch (PathTooLongException) { // otherwise we fail to create it completely... } diff --git a/Nodejs/Tests/AnalysisTests/AnalysisFile.cs b/Nodejs/Tests/AnalysisTests/AnalysisFile.cs index c0e73f886..318205370 100644 --- a/Nodejs/Tests/AnalysisTests/AnalysisFile.cs +++ b/Nodejs/Tests/AnalysisTests/AnalysisFile.cs @@ -132,9 +132,7 @@ public static JsAnalyzer Analyze(string directory, AnalysisLimits limits = null, foreach (var file in Directory.GetFiles(directory, "*", SearchOption.AllDirectories)) { if (String.Equals(Path.GetExtension(file), ".js", StringComparison.OrdinalIgnoreCase)) { var relativeFile = file.Substring(directory.Length); - if (!limits.IsPathExceedNestingLimit(relativeFile)) { - files.Add(new AnalysisFile(file, File.ReadAllText(file))); - } + files.Add(new AnalysisFile(file, File.ReadAllText(file))); } else if (String.Equals(Path.GetFileName(file), "package.json", StringComparison.OrdinalIgnoreCase)) { JavaScriptSerializer serializer = new JavaScriptSerializer(); Dictionary json;