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;