diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index b1e3ffbdac6a3..0e2cff7d00487 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -340,10 +340,7 @@ namespace ts { } const failedLookupLocations: string[] = []; - const features = - getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 ? NodeResolutionFeatures.Node12Default : - getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault : - NodeResolutionFeatures.None; + const features = getDefaultNodeResolutionFeatures(options); const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions: ["node", "require", "types"] }; let resolved = primaryLookup(); let primary = true; @@ -433,6 +430,42 @@ namespace ts { } } + function getDefaultNodeResolutionFeatures(options: CompilerOptions) { + return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 ? NodeResolutionFeatures.Node12Default : + getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault : + NodeResolutionFeatures.None; + } + + /** + * @internal + * Does not try `@types/${packageName}` - use a second pass if needed. + */ + export function resolvePackageNameToPackageJson( + packageName: string, + containingDirectory: string, + options: CompilerOptions, + host: ModuleResolutionHost, + cache: ModuleResolutionCache | undefined, + ): PackageJsonInfo | undefined { + const moduleResolutionState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache: cache?.getPackageJsonInfoCache(), + conditions: emptyArray, + features: NodeResolutionFeatures.None, + }; + + return forEachAncestorDirectory(containingDirectory, ancestorDirectory => { + if (getBaseFileName(ancestorDirectory) !== "node_modules") { + const nodeModulesFolder = combinePaths(ancestorDirectory, "node_modules"); + const candidate = combinePaths(nodeModulesFolder, packageName); + return getPackageJsonInfo(candidate, /*onlyRecordFailures*/ false, moduleResolutionState); + } + }); + } + /** * Given a set of options, returns the set of type directive names * that should be included for this program automatically. @@ -1171,11 +1204,6 @@ namespace ts { return resolvedModule.resolvedFileName; } - /* @internal */ - export function tryResolveJSModule(moduleName: string, initialDir: string, host: ModuleResolutionHost) { - return tryResolveJSModuleWorker(moduleName, initialDir, host).resolvedModule; - } - /* @internal */ enum NodeResolutionFeatures { None = 0, @@ -1536,11 +1564,124 @@ namespace ts { return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths)); } + /* @internal */ + export function getEntrypointsFromPackageJsonInfo( + packageJsonInfo: PackageJsonInfo, + options: CompilerOptions, + host: ModuleResolutionHost, + cache: ModuleResolutionCache | undefined, + resolveJs?: boolean, + ): string[] | false { + if (!resolveJs && packageJsonInfo.resolvedEntrypoints !== undefined) { + // Cached value excludes resolutions to JS files - those could be + // cached separately, but they're used rarely. + return packageJsonInfo.resolvedEntrypoints; + } + + let entrypoints: string[] | undefined; + const extensions = resolveJs ? Extensions.JavaScript : Extensions.TypeScript; + const features = getDefaultNodeResolutionFeatures(options); + const requireState: ModuleResolutionState = { + compilerOptions: options, + host, + traceEnabled: isTraceEnabled(options, host), + failedLookupLocations: [], + packageJsonInfoCache: cache?.getPackageJsonInfoCache(), + conditions: ["node", "require", "types"], + features, + }; + const requireResolution = loadNodeModuleFromDirectoryWorker( + extensions, + packageJsonInfo.packageDirectory, + /*onlyRecordFailures*/ false, + requireState, + packageJsonInfo.packageJsonContent, + packageJsonInfo.versionPaths); + entrypoints = append(entrypoints, requireResolution?.path); + + if (features & NodeResolutionFeatures.Exports && packageJsonInfo.packageJsonContent.exports) { + for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) { + const exportState = { ...requireState, failedLookupLocations: [], conditions }; + const exportResolutions = loadEntrypointsFromExportMap( + packageJsonInfo, + packageJsonInfo.packageJsonContent.exports, + exportState, + extensions); + if (exportResolutions) { + for (const resolution of exportResolutions) { + entrypoints = appendIfUnique(entrypoints, resolution.path); + } + } + } + } + + return packageJsonInfo.resolvedEntrypoints = entrypoints || false; + } + + function loadEntrypointsFromExportMap( + scope: PackageJsonInfo, + exports: object, + state: ModuleResolutionState, + extensions: Extensions, + ): PathAndExtension[] | undefined { + let entrypoints: PathAndExtension[] | undefined; + if (isArray(exports)) { + for (const target of exports) { + loadEntrypointsFromTargetExports(target); + } + } + // eslint-disable-next-line no-null/no-null + else if (typeof exports === "object" && exports !== null && allKeysStartWithDot(exports as MapLike)) { + for (const key in exports) { + loadEntrypointsFromTargetExports((exports as MapLike)[key]); + } + } + else { + loadEntrypointsFromTargetExports(exports); + } + return entrypoints; + + function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined { + if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) { + const partsAfterFirst = getPathComponents(target).slice(2); + if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { + return false; + } + const resolvedTarget = combinePaths(scope.packageDirectory, target); + const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); + const result = loadJSOrExactTSFileName(extensions, finalPath, /*recordOnlyFailures*/ false, state); + if (result) { + entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); + return true; + } + } + else if (Array.isArray(target)) { + for (const t of target) { + const success = loadEntrypointsFromTargetExports(t); + if (success) { + return true; + } + } + } + // eslint-disable-next-line no-null/no-null + else if (typeof target === "object" && target !== null) { + return forEach(getOwnKeys(target as MapLike), key => { + if (key === "default" || contains(state.conditions, key) || isApplicableVersionedTypesKey(state.conditions, key)) { + loadEntrypointsFromTargetExports((target as MapLike)[key]); + return true; + } + }); + } + } + } + /*@internal*/ interface PackageJsonInfo { packageDirectory: string; packageJsonContent: PackageJsonPathFields; versionPaths: VersionPaths | undefined; + /** false: resolved to nothing. undefined: not yet resolved */ + resolvedEntrypoints: string[] | false | undefined; } /** @@ -1606,7 +1747,7 @@ namespace ts { trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath); } const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state); - const result = { packageDirectory, packageJsonContent, versionPaths }; + const result = { packageDirectory, packageJsonContent, versionPaths, resolvedEntrypoints: undefined }; state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result); return result; } diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index cb2bad2e27166..c17dfeabcf1e7 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -59,19 +59,29 @@ namespace ts.moduleSpecifiers { }; } + // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? + // Because when this is called by the file renamer, `importingSourceFile` is the file being renamed, + // while `importingSourceFileName` its *new* name. We need a source file just to get its + // `impliedNodeFormat` and to detect certain preferences from existing import module specifiers. export function updateModuleSpecifier( compilerOptions: CompilerOptions, + importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, oldImportSpecifier: string, ): string | undefined { - const res = getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}); + const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}); if (res === oldImportSpecifier) return undefined; return res; } - // Note: importingSourceFile is just for usesJsExtensionOnImports + // `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`? + // Because when this is called by the declaration emitter, `importingSourceFile` is the implementation + // file, but `importingSourceFileName` and `toFileName` refer to declaration files (the former to the + // one currently being produced; the latter to the one being imported). We need an implementation file + // just to get its `impliedNodeFormat` and to detect certain preferences from existing import module + // specifiers. export function getModuleSpecifier( compilerOptions: CompilerOptions, importingSourceFile: SourceFile, @@ -79,24 +89,25 @@ namespace ts.moduleSpecifiers { toFileName: string, host: ModuleSpecifierResolutionHost, ): string { - return getModuleSpecifierWorker(compilerOptions, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}); + return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}); } export function getNodeModulesPackageName( compilerOptions: CompilerOptions, - importingSourceFileName: Path, + importingSourceFile: SourceFile, nodeModulesFileName: string, host: ModuleSpecifierResolutionHost, preferences: UserPreferences, ): string | undefined { - const info = getInfo(importingSourceFileName, host); - const modulePaths = getAllModulePaths(importingSourceFileName, nodeModulesFileName, host, preferences); + const info = getInfo(importingSourceFile.path, host); + const modulePaths = getAllModulePaths(importingSourceFile.path, nodeModulesFileName, host, preferences); return firstDefined(modulePaths, - modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions, /*packageNameOnly*/ true)); + modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions, /*packageNameOnly*/ true)); } function getModuleSpecifierWorker( compilerOptions: CompilerOptions, + importingSourceFile: SourceFile, importingSourceFileName: Path, toFileName: string, host: ModuleSpecifierResolutionHost, @@ -105,7 +116,7 @@ namespace ts.moduleSpecifiers { ): string { const info = getInfo(importingSourceFileName, host); const modulePaths = getAllModulePaths(importingSourceFileName, toFileName, host, userPreferences); - return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions)) || + return firstDefined(modulePaths, modulePath => tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions)) || getLocalModuleSpecifier(toFileName, info, compilerOptions, host, preferences); } @@ -222,7 +233,7 @@ namespace ts.moduleSpecifiers { let pathsSpecifiers: string[] | undefined; let relativeSpecifiers: string[] | undefined; for (const modulePath of modulePaths) { - const specifier = tryGetModuleNameAsNodeModule(modulePath, info, host, compilerOptions); + const specifier = tryGetModuleNameAsNodeModule(modulePath, info, importingSourceFile, host, compilerOptions); nodeModulesSpecifiers = append(nodeModulesSpecifiers, specifier); if (specifier && modulePath.isRedirect) { // If we got a specifier for a redirect, it was a bare package specifier (e.g. "@foo/bar", @@ -639,7 +650,7 @@ namespace ts.moduleSpecifiers { : removeFileExtension(relativePath); } - function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { + function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile , host: ModuleSpecifierResolutionHost, options: CompilerOptions, packageNameOnly?: boolean): string | undefined { if (!host.fileExists || !host.readFile) { return undefined; } @@ -706,11 +717,19 @@ namespace ts.moduleSpecifiers { let moduleFileToTry = path; if (host.fileExists(packageJsonPath)) { const packageJsonContent = JSON.parse(host.readFile!(packageJsonPath)!); - // TODO: Inject `require` or `import` condition based on the intended import mode if (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext) { - const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" ? tryGetModuleNameFromExports(options, path, packageRootPath, packageJsonContent.name, packageJsonContent.exports, ["node", "types"]) : undefined; + // `conditions` *could* be made to go against `importingSourceFile.impliedNodeFormat` if something wanted to generate + // an ImportEqualsDeclaration in an ESM-implied file or an ImportCall in a CJS-implied file. But since this function is + // usually called to conjure an import out of thin air, we don't have an existing usage to call `getModeForUsageAtIndex` + // with, so for now we just stick with the mode of the file. + const conditions = ["node", importingSourceFile.impliedNodeFormat === ModuleKind.ESNext ? "import" : "require", "types"]; + const fromExports = packageJsonContent.exports && typeof packageJsonContent.name === "string" + ? tryGetModuleNameFromExports(options, path, packageRootPath, getPackageNameFromTypesPackageName(packageJsonContent.name), packageJsonContent.exports, conditions) + : undefined; if (fromExports) { - const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) ? fromExports : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; + const withJsExtension = !hasTSFileExtension(fromExports.moduleFileToTry) + ? fromExports + : { moduleFileToTry: removeFileExtension(fromExports.moduleFileToTry) + tryGetJSExtensionForFile(fromExports.moduleFileToTry, options) }; return { ...withJsExtension, verbatimFromExports: true }; } if (packageJsonContent.exports) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3024b8eba1567..98d89d4612824 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -6307,8 +6307,6 @@ namespace ts { getSymlinkedFiles(): ReadonlyESMap | undefined; setSymlinkedDirectory(symlink: string, real: SymlinkedDirectory | false): void; setSymlinkedFile(symlinkPath: Path, real: string): void; - /*@internal*/ - setSymlinkedDirectoryFromSymlinkedFile(symlink: string, real: string): void; /** * @internal * Uses resolvedTypeReferenceDirectives from program instead of from files, since files @@ -6346,16 +6344,6 @@ namespace ts { (symlinkedDirectories || (symlinkedDirectories = new Map())).set(symlinkPath, real); } }, - setSymlinkedDirectoryFromSymlinkedFile(symlink, real) { - this.setSymlinkedFile(toPath(symlink, cwd, getCanonicalFileName), real); - const [commonResolved, commonOriginal] = guessDirectorySymlink(real, symlink, cwd, getCanonicalFileName) || emptyArray; - if (commonResolved && commonOriginal) { - this.setSymlinkedDirectory(commonOriginal, { - real: commonResolved, - realPath: toPath(commonResolved, cwd, getCanonicalFileName), - }); - } - }, setSymlinksFromResolutions(files, typeReferenceDirectives) { Debug.assert(!hasProcessedResolutions); hasProcessedResolutions = true; diff --git a/src/server/project.ts b/src/server/project.ts index e6170d086ce81..24ceaab91f24e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1925,34 +1925,55 @@ namespace ts.server { } if (dependencyNames) { - const resolutions = mapDefined(arrayFrom(dependencyNames.keys()), name => { - const types = resolveTypeReferenceDirective( + let dependenciesAdded = 0; + const symlinkCache = hostProject.getSymlinkCache(); + for (const name of arrayFrom(dependencyNames.keys())) { + // Avoid creating a large project that would significantly slow down time to editor interactivity + if (dependencySelection === PackageJsonAutoImportPreference.Auto && dependenciesAdded > this.maxDependencies) { + hostProject.log(`Auto-import provider attempted to add more than ${this.maxDependencies} dependencies.`); + return ts.emptyArray; + } + + // 1. Try to load from the implementation package. For many dependencies, the + // package.json will exist, but the package will not contain any typings, + // so `entrypoints` will be undefined. In that case, or if the dependency + // is missing altogether, we will move on to trying the @types package (2). + const packageJson = resolvePackageNameToPackageJson( name, - rootFileName, + hostProject.currentDirectory, compilerOptions, - moduleResolutionHost); - - if (types.resolvedTypeReferenceDirective) { - return types.resolvedTypeReferenceDirective; - } - if (compilerOptions.allowJs && compilerOptions.maxNodeModuleJsDepth) { - return tryResolveJSModule(name, hostProject.currentDirectory, moduleResolutionHost); + moduleResolutionHost, + program.getModuleResolutionCache()); + if (packageJson) { + const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache); + if (entrypoints) { + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints.length ? 1 : 0; + continue; + } } - }); - const symlinkCache = hostProject.getSymlinkCache(); - for (const resolution of resolutions) { - if (!resolution.resolvedFileName) continue; - const { resolvedFileName, originalPath } = resolution; - if (originalPath) { - symlinkCache.setSymlinkedDirectoryFromSymlinkedFile(originalPath, resolvedFileName); + // 2. Try to load from the @types package. + const typesPackageJson = resolvePackageNameToPackageJson( + `@types/${name}`, + hostProject.currentDirectory, + compilerOptions, + moduleResolutionHost, + program.getModuleResolutionCache()); + if (typesPackageJson) { + const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache); + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints?.length ? 1 : 0; + continue; } - if (!program.getSourceFile(resolvedFileName) && (!originalPath || !program.getSourceFile(originalPath))) { - rootNames = append(rootNames, resolvedFileName); - // Avoid creating a large project that would significantly slow down time to editor interactivity - if (dependencySelection === PackageJsonAutoImportPreference.Auto && rootNames.length > this.maxDependencies) { - return ts.emptyArray; - } + + // 3. If the @types package did not exist and the user has settings that + // allow processing JS from node_modules, go back to the implementation + // package and load the JS. + if (packageJson && compilerOptions.allowJs && compilerOptions.maxNodeModuleJsDepth) { + const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache, /*allowJs*/ true); + rootNames = concatenate(rootNames, entrypoints); + dependenciesAdded += entrypoints?.length ? 1 : 0; } } } @@ -1964,6 +1985,33 @@ namespace ts.server { (dependencyNames || (dependencyNames = new Set())).add(dependency); } } + + type PackageJsonInfo = NonNullable>; + function getRootNamesFromPackageJson(packageJson: PackageJsonInfo, program: Program, symlinkCache: SymlinkCache, resolveJs?: boolean) { + const entrypoints = getEntrypointsFromPackageJsonInfo( + packageJson, + compilerOptions, + moduleResolutionHost, + program.getModuleResolutionCache(), + resolveJs); + if (entrypoints) { + const real = moduleResolutionHost.realpath?.(packageJson.packageDirectory); + const isSymlink = real && real !== packageJson.packageDirectory; + if (isSymlink) { + symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, { + real, + realPath: hostProject.toPath(real), + }); + } + + return mapDefined(entrypoints, entrypoint => { + const resolvedFileName = isSymlink ? entrypoint.replace(packageJson.packageDirectory, real) : entrypoint; + if (!program.getSourceFile(resolvedFileName) && !(isSymlink && program.getSourceFile(entrypoint))) { + return resolvedFileName; + } + }); + } + } } /*@internal*/ diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index fa0749df0cecc..369f8e8e23f07 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -156,7 +156,7 @@ namespace ts { // Need an update if the imported file moved, or the importing file moved and was using a relative path. return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) - ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text) + ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), sourceFile, getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text) : undefined; }); } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index a45fd3f441ecf..b204d67023970 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3085,7 +3085,7 @@ namespace ts { } const specifier = moduleSpecifiers.getNodeModulesPackageName( host.getCompilationSettings(), - fromFile.path, + fromFile, importedFileName, moduleSpecifierResolutionHost, preferences, diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 5eb69c514cd1b..0659c78de2748 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -19,6 +19,10 @@ namespace ts.projectSystem { path: "/ambient.d.ts", content: "declare module 'ambient' {}" }; + const mobxPackageJson: File = { + path: "/node_modules/mobx/package.json", + content: `{ "name": "mobx", "version": "1.0.0" }` + }; const mobxDts: File = { path: "/node_modules/mobx/index.d.ts", content: "export declare function observable(): unknown;" @@ -118,7 +122,7 @@ namespace ts.projectSystem { }); function setup() { - const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts, exportEqualsMappedType]); + const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts, exportEqualsMappedType]); const session = createSession(host); openFilesForSession([aTs, bTs], session); const projectService = session.getProjectService(); diff --git a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts index fd57438a2691e..63434784b0cf4 100644 --- a/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts +++ b/src/testRunner/unittests/tsserver/moduleSpecifierCache.ts @@ -27,6 +27,10 @@ namespace ts.projectSystem { path: "/src/ambient.d.ts", content: "declare module 'ambient' {}" }; + const mobxPackageJson: File = { + path: "/node_modules/mobx/package.json", + content: `{ "name": "mobx", "version": "1.0.0" }` + }; const mobxDts: File = { path: "/node_modules/mobx/index.d.ts", content: "export declare function observable(): unknown;" @@ -120,7 +124,7 @@ namespace ts.projectSystem { }); function setup() { - const host = createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxDts]); + const host = createServerHost([aTs, bTs, cTs, bSymlink, ambientDeclaration, tsconfig, packageJson, mobxPackageJson, mobxDts]); const session = createSession(host); openFilesForSession([aTs, bTs, cTs], session); const projectService = session.getProjectService(); diff --git a/src/testRunner/unittests/tsserver/symlinkCache.ts b/src/testRunner/unittests/tsserver/symlinkCache.ts index 7a80a0cc289f2..f9877c4c1b85c 100644 --- a/src/testRunner/unittests/tsserver/symlinkCache.ts +++ b/src/testRunner/unittests/tsserver/symlinkCache.ts @@ -60,7 +60,12 @@ namespace ts.projectSystem { it("works for paths close to the root", () => { const cache = createSymlinkCache("/", createGetCanonicalFileName(/*useCaseSensitiveFileNames*/ false)); - cache.setSymlinkedDirectoryFromSymlinkedFile("/foo", "/one/two/foo"); // Used to crash, #44953 + // Used to crash, #44953 + cache.setSymlinksFromResolutions([], new Map([["foo", { + primary: true, + originalPath: "/foo", + resolvedFileName: "/one/two/foo", + }]])); }); }); diff --git a/tests/cases/fourslash/server/autoImportProvider8.ts b/tests/cases/fourslash/server/autoImportProvider8.ts new file mode 100644 index 0000000000000..28a29e28e9bb2 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider8.ts @@ -0,0 +1,68 @@ +/// + +// @Filename: /tsconfig.json +//// { "compilerOptions": { "module": "commonjs" } } + +// @Filename: /package.json +//// { "dependencies": { "mylib": "file:packages/mylib" } } + +// @Filename: /packages/mylib/package.json +//// { "name": "mylib", "version": "1.0.0" } + +// @Filename: /packages/mylib/index.ts +//// export * from "./mySubDir"; + +// @Filename: /packages/mylib/mySubDir/index.ts +//// export * from "./myClass"; +//// export * from "./myClass2"; + +// @Filename: /packages/mylib/mySubDir/myClass.ts +//// export class MyClass {} + +// @Filename: /packages/mylib/mySubDir/myClass2.ts +//// export class MyClass2 {} + +// @link: /packages/mylib -> /node_modules/mylib + +// @Filename: /src/index.ts +//// +//// const a = new MyClass/*1*/(); +//// const b = new MyClass2/*2*/(); + +goTo.marker("1"); +format.setOption("newLineCharacter", "\n"); + +verify.completions({ + marker: "1", + includes: [{ + name: "MyClass", + source: "mylib", + sourceDisplay: "mylib", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }], + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + } +}); + +verify.applyCodeActionFromCompletion("1", { + name: "MyClass", + source: "mylib", + description: `Import 'MyClass' from module "mylib"`, + data: { + exportName: "MyClass", + fileName: "/packages/mylib/index.ts", + }, + preferences: { + includeCompletionsForModuleExports: true, + includeCompletionsWithInsertText: true, + allowIncompleteCompletions: true, + }, + newFileContent: `import { MyClass } from "mylib"; + +const a = new MyClass(); +const b = new MyClass2();`, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap1.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap1.ts new file mode 100644 index 0000000000000..7f76a8fae02f2 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap1.ts @@ -0,0 +1,64 @@ +/// + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": { +//// "types": "./lib/index.d.ts" +//// }, +//// "./lol": { +//// "types": "./lib/lol.d.ts" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap2.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap2.ts new file mode 100644 index 0000000000000..3d01e53006b6e --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap2.ts @@ -0,0 +1,61 @@ +/// + +// This one uses --module=commonjs, so the export map is not followed. + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "commonjs" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "types": "./lib/index.d.ts", +//// "exports": { +//// ".": { +//// "types": "./lib/index.d.ts" +//// }, +//// "./lol": { +//// "types": "./lib/lol.d.ts" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts new file mode 100644 index 0000000000000..e4e70c749fefa --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap3.ts @@ -0,0 +1,63 @@ +/// + +// String exports + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "name": "dependency", +//// "version": "1.0.0", +//// "main": "./lib/index.js", +//// "exports": "./lib/lol.d.ts" +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + // TODO: We should filter this one out due to its bad module specifier, + // but we don't know it's going to be filtered out until we actually + // resolve the module specifier, which is a problem for completions + // that don't have their module specifiers eagerly resolved. + name: "fooFromIndex", + source: "../node_modules/dependency/lib/index.js", + sourceDisplay: "../node_modules/dependency/lib/index.js", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap4.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap4.ts new file mode 100644 index 0000000000000..638bbf70b023b --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap4.ts @@ -0,0 +1,56 @@ +/// + +// Top-level conditions + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// "types": "./lib/index.d.ts", +//// "require": "./lib/lol.js" +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap5.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap5.ts new file mode 100644 index 0000000000000..7bc8946f7f027 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap5.ts @@ -0,0 +1,79 @@ +/// + +// @types package lookup + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.js", +//// "./lol": "./lib/lol.js" +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.js +//// export function fooFromIndex() {} + +// @Filename: /node_modules/dependency/lib/lol.js +//// export function fooFromLol() {} + +// @Filename: /node_modules/@types/dependency/package.json +//// { +//// "type": "module", +//// "name": "@types/dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.d.ts", +//// "./lol": "./lib/lol.d.ts" +//// } +//// } + +// @Filename: /node_modules/@types/dependency/lib/index.d.ts +//// export declare function fooFromIndex(): void; + +// @Filename: /node_modules/@types/dependency/lib/lol.d.ts +//// export declare function fooFromLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap6.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap6.ts new file mode 100644 index 0000000000000..19a32344ae60d --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap6.ts @@ -0,0 +1,88 @@ +/// + +// @types package should be ignored because implementation package has types + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// }, +//// "devDependencies": { +//// "@types/dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.js", +//// "./lol": "./lib/lol.js" +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.js +//// export function fooFromIndex() {} + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export declare function fooFromIndex(): void + +// @Filename: /node_modules/dependency/lib/lol.js +//// export function fooFromLol() {} + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export declare function fooFromLol(): void + +// @Filename: /node_modules/@types/dependency/package.json +//// { +//// "type": "module", +//// "name": "@types/dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": "./lib/index.d.ts", +//// "./lol": "./lib/lol.d.ts" +//// } +//// } + +// @Filename: /node_modules/@types/dependency/lib/index.d.ts +//// export declare function fooFromAtTypesIndex(): void; + +// @Filename: /node_modules/@types/dependency/lib/lol.d.ts +//// export declare function fooFromAtTypesLol(): void; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap7.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap7.ts new file mode 100644 index 0000000000000..d1148f824fbe1 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap7.ts @@ -0,0 +1,69 @@ +/// + +// Some exports are already in the main program while some are not. + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// ".": { +//// "types": "./lib/index.d.ts" +//// }, +//// "./lol": { +//// "types": "./lib/lol.d.ts" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/bar.ts +//// import { fooFromIndex } from "dependency"; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency", + sourceDisplay: "dependency", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); \ No newline at end of file diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts new file mode 100644 index 0000000000000..b679c2184fd50 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap8.ts @@ -0,0 +1,94 @@ +/// + +// Both 'import' and 'require' should be pulled in + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// "./lol": { +//// "import": "./lib/index.js", +//// "require": "./lib/lol.js" +//// } +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/bar.ts +//// import { fooFromIndex } from "dependency"; + +// @Filename: /src/foo.cts +//// fooFrom/*cts*/ + +// @Filename: /src/foo.mts +//// fooFrom/*mts*/ + +goTo.marker("cts"); +verify.completions({ + marker: "cts", + exact: completion.globalsPlus([{ + // TODO: this one will go away (see note in ./autoImportProvider_exportMap3.ts) + name: "fooFromIndex", + source: "../node_modules/dependency/lib/index", + sourceDisplay: "../node_modules/dependency/lib/index", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + name: "fooFromLol", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); + +goTo.marker("mts"); +verify.completions({ + marker: "mts", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }, { + // TODO: this one will go away (see note in ./autoImportProvider_exportMap3.ts) + name: "fooFromLol", + source: "../node_modules/dependency/lib/lol.js", + sourceDisplay: "../node_modules/dependency/lib/lol.js", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/server/autoImportProvider_exportMap9.ts b/tests/cases/fourslash/server/autoImportProvider_exportMap9.ts new file mode 100644 index 0000000000000..e7768c984321a --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_exportMap9.ts @@ -0,0 +1,58 @@ +/// + +// Only the first resolution in an array should be used + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "dependency": "^1.0.0" +//// } +//// } + +// @Filename: /node_modules/dependency/package.json +//// { +//// "type": "module", +//// "name": "dependency", +//// "version": "1.0.0", +//// "exports": { +//// "./lol": ["./lib/index.js", "./lib/lol.js"] +//// } +//// } + +// @Filename: /node_modules/dependency/lib/index.d.ts +//// export function fooFromIndex(): void; + +// @Filename: /node_modules/dependency/lib/lol.d.ts +//// export function fooFromLol(): void; + +// @Filename: /src/bar.ts +//// import { fooFromIndex } from "dependency"; + +// @Filename: /src/foo.ts +//// fooFrom/**/ + + +goTo.marker(""); +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "fooFromIndex", + source: "dependency/lol", + sourceDisplay: "dependency/lol", + sortText: completion.SortText.AutoImportSuggestions, + hasAction: true, + }]), + preferences: { + includeCompletionsForModuleExports: true, + includeInsertTextCompletions: true, + allowIncompleteCompletions: true, + }, +}); diff --git a/tests/cases/fourslash/server/completionsImport_mergedReExport.ts b/tests/cases/fourslash/server/completionsImport_mergedReExport.ts index 63d813a0c9866..a65e2a115990d 100644 --- a/tests/cases/fourslash/server/completionsImport_mergedReExport.ts +++ b/tests/cases/fourslash/server/completionsImport_mergedReExport.ts @@ -6,6 +6,9 @@ // @Filename: /package.json //// { "dependencies": { "@jest/types": "*", "ts-jest": "*" } } +// @Filename: /node_modules/@jest/types/package.json +//// { "name": "@jest/types" } + // @Filename: /node_modules/@jest/types/index.d.ts //// import type * as Config from "./Config"; //// export type { Config };