diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index a203b2bfaba35..29d582e787435 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -117,7 +117,8 @@ namespace ts { } } - function readJson(path: string, host: ModuleResolutionHost): PackageJson { + /* @internal */ + export function readJson(path: string, host: { readFile(fileName: string): string | undefined }): object { try { const jsonText = host.readFile(path); return jsonText ? JSON.parse(jsonText) : {}; @@ -300,7 +301,7 @@ namespace ts { // `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types. // See `createNotNeededPackageJSON` in the types-publisher` repo. // tslint:disable-next-line:no-null-keyword - const isNotNeededPackage = host.fileExists(packageJsonPath) && readJson(packageJsonPath, host).typings === null; + const isNotNeededPackage = host.fileExists(packageJsonPath) && (readJson(packageJsonPath, host) as PackageJson).typings === null; if (!isNotNeededPackage) { // Return just the type directive names result.push(getBaseFileName(normalized)); @@ -983,7 +984,7 @@ namespace ts { const directoryExists = !onlyRecordFailures && directoryProbablyExists(nodeModuleDirectory, host); const packageJsonPath = pathToPackageJson(nodeModuleDirectory); if (directoryExists && host.fileExists(packageJsonPath)) { - const packageJsonContent = readJson(packageJsonPath, host); + const packageJsonContent = readJson(packageJsonPath, host) as PackageJson; if (subModuleName === "") { // looking up the root - need to handle types/typings/main redirects for subModuleName const path = tryReadPackageJsonFields(/*readTypes*/ true, packageJsonContent, nodeModuleDirectory, state); if (typeof path === "string") { diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index 976449efee5b8..99ce0afb36cdb 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -137,8 +137,9 @@ namespace ts.Completions.PathCompletions { if (directories) { for (const directory of directories) { const directoryName = getBaseFileName(normalizePath(directory)); - - result.push(nameAndKind(directoryName, ScriptElementKind.directory)); + if (directoryName !== "@types") { + result.push(nameAndKind(directoryName, ScriptElementKind.directory)); + } } } } @@ -177,19 +178,33 @@ namespace ts.Completions.PathCompletions { } } - if (compilerOptions.moduleResolution === ModuleResolutionKind.NodeJs) { - forEachAncestorDirectory(scriptPath, ancestor => { - const nodeModules = combinePaths(ancestor, "node_modules"); - if (host.directoryExists(nodeModules)) { - getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result); - } - }); + const fragmentDirectory = containsSlash(fragment) ? getDirectoryPath(fragment) : undefined; + for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) { + result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName)); } getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, result); - for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) { - result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName)); + if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) { + // If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies. + // (But do if we didn't find anything, e.g. 'package.json' missing.) + let foundGlobal = false; + if (fragmentDirectory === undefined) { + for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) { + if (!result.some(entry => entry.name === moduleName)) { + foundGlobal = true; + result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName)); + } + } + } + if (!foundGlobal) { + forEachAncestorDirectory(scriptPath, ancestor => { + const nodeModules = combinePaths(ancestor, "node_modules"); + if (tryDirectoryExists(host, nodeModules)) { + getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result); + } + }); + } } return result; @@ -228,7 +243,7 @@ namespace ts.Completions.PathCompletions { const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix); const normalizedPrefixBase = getBaseFileName(normalizedPrefix); - const fragmentHasPath = stringContains(fragment, directorySeparator); + const fragmentHasPath = containsSlash(fragment); // Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory; @@ -262,45 +277,19 @@ namespace ts.Completions.PathCompletions { return path[0] === directorySeparator ? path.slice(1) : path; } - function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] { - // Check If this is a nested module - const isNestedModule = stringContains(fragment, directorySeparator); - const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined; - + function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): ReadonlyArray { // Get modules that the type checker picked up - const ambientModules = map(typeChecker.getAmbientModules(), sym => stripQuotes(sym.name)); - let nonRelativeModuleNames = filter(ambientModules, moduleName => startsWith(moduleName, fragment)); + const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name)); + const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment)); // Nested modules of the form "module-name/sub" need to be adjusted to only return the string // after the last '/' that appears in the fragment because that's where the replacement span // starts - if (isNestedModule) { - const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment); - nonRelativeModuleNames = map(nonRelativeModuleNames, nonRelativeModuleName => { - return removePrefix(nonRelativeModuleName, moduleNameWithSeperator); - }); - } - - - if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) { - for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) { - if (!isNestedModule) { - nonRelativeModuleNames.push(visibleModule.moduleName); - } - else if (startsWith(visibleModule.moduleName, moduleNameFragment)) { - const nestedFiles = tryReadDirectory(host, visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/ undefined, /*include*/ ["./*"]); - if (nestedFiles) { - for (let f of nestedFiles) { - f = normalizePath(f); - const nestedModule = removeFileExtension(getBaseFileName(f)); - nonRelativeModuleNames.push(nestedModule); - } - } - } - } + if (fragmentDirectory !== undefined) { + const moduleNameWithSeperator = ensureTrailingDirectorySeparator(fragmentDirectory); + return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeperator)); } - - return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive); + return nonRelativeModuleNames; } export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): ReadonlyArray | undefined { @@ -390,48 +379,16 @@ namespace ts.Completions.PathCompletions { return paths; } - function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string) { - const result: VisibleModuleInfo[] = []; - - if (host.readFile && host.fileExists) { - for (const packageJson of findPackageJsons(scriptPath, host)) { - const contents = tryReadingPackageJson(packageJson); - if (!contents) { - return; - } + function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): ReadonlyArray { + if (!host.readFile || !host.fileExists) return emptyArray; - const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules"); - const foundModuleNames: string[] = []; - - // Provide completions for all non @types dependencies - for (const key of nodeModulesDependencyKeys) { - addPotentialPackageNames(contents[key], foundModuleNames); - } - - for (const moduleName of foundModuleNames) { - const moduleDir = combinePaths(nodeModulesDir, moduleName); - result.push({ - moduleName, - moduleDir - }); - } - } - } - - return result; - - function tryReadingPackageJson(filePath: string) { - try { - const fileText = tryReadFile(host, filePath); - return fileText ? JSON.parse(fileText) : undefined; - } - catch (e) { - return undefined; - } - } - - function addPotentialPackageNames(dependencies: any, result: string[]) { - if (dependencies) { + const result: string[] = []; + for (const packageJson of findPackageJsons(scriptPath, host)) { + const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined }); // Cast to assert that readFile is defined + // Provide completions for all non @types dependencies + for (const key of nodeModulesDependencyKeys) { + const dependencies: object | undefined = (contents as any)[key]; + if (!dependencies) continue; for (const dep in dependencies) { if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) { result.push(dep); @@ -439,6 +396,7 @@ namespace ts.Completions.PathCompletions { } } } + return result; } // Replace everything after the last directory seperator that appears @@ -484,11 +442,6 @@ namespace ts.Completions.PathCompletions { */ const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*