diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index ac83dd41311..08178d3d16a 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -77,16 +77,20 @@ namespace ts { traceEnabled: boolean; } - interface PackageJson { - name?: string; - version?: string; + /** Just the fields that we use for module resolution. */ + interface PackageJsonPathFields { typings?: string; types?: string; main?: string; } + interface PackageJson extends PackageJsonPathFields { + name?: string; + version?: string; + } + /** Reads from "main" or "types"/"typings" depending on `extensions`. */ - function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJson, baseDirectory: string, state: ModuleResolutionState): string | undefined { + function tryReadPackageJsonFields(readTypes: boolean, jsonContent: PackageJsonPathFields, baseDirectory: string, state: ModuleResolutionState): string | undefined { return readTypes ? tryReadFromField("typings") || tryReadFromField("types") : tryReadFromField("main"); function tryReadFromField(fieldName: "typings" | "types" | "main"): string | undefined { @@ -886,7 +890,7 @@ namespace ts { return withPackageId(packageId, loadNodeModuleFromDirectoryWorker(extensions, candidate, failedLookupLocations, onlyRecordFailures, state, packageJsonContent)); } - function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, failedLookupLocations: Push, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonContent: PackageJson | undefined): PathAndExtension | undefined { + function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: string, failedLookupLocations: Push, onlyRecordFailures: boolean, state: ModuleResolutionState, packageJsonContent: PackageJsonPathFields | undefined): PathAndExtension | undefined { const fromPackageJson = packageJsonContent && loadModuleFromPackageJson(packageJsonContent, extensions, candidate, failedLookupLocations, state); if (fromPackageJson) { return fromPackageJson; @@ -901,7 +905,7 @@ namespace ts { failedLookupLocations: Push, onlyRecordFailures: boolean, { host, traceEnabled }: ModuleResolutionState, - ): { packageJsonContent: PackageJson | undefined, packageId: PackageId | undefined } { + ): { found: boolean, packageJsonContent: PackageJsonPathFields | undefined, packageId: PackageId | undefined } { const directoryExists = !onlyRecordFailures && directoryProbablyExists(nodeModuleDirectory, host); const packageJsonPath = pathToPackageJson(nodeModuleDirectory); if (directoryExists && host.fileExists(packageJsonPath)) { @@ -912,7 +916,7 @@ namespace ts { const packageId: PackageId = typeof packageJsonContent.name === "string" && typeof packageJsonContent.version === "string" ? { name: packageJsonContent.name, subModuleName, version: packageJsonContent.version } : undefined; - return { packageJsonContent, packageId }; + return { found: true, packageJsonContent, packageId }; } else { if (directoryExists && traceEnabled) { @@ -920,11 +924,11 @@ namespace ts { } // record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results failedLookupLocations.push(packageJsonPath); - return { packageJsonContent: undefined, packageId: undefined }; + return { found: false, packageJsonContent: undefined, packageId: undefined }; } } - function loadModuleFromPackageJson(jsonContent: PackageJson, extensions: Extensions, candidate: string, failedLookupLocations: Push, state: ModuleResolutionState): PathAndExtension | undefined { + function loadModuleFromPackageJson(jsonContent: PackageJsonPathFields, extensions: Extensions, candidate: string, failedLookupLocations: Push, state: ModuleResolutionState): PathAndExtension | undefined { const file = tryReadPackageJsonFields(extensions !== Extensions.JavaScript, jsonContent, candidate, state); if (!file) { return undefined; @@ -976,10 +980,22 @@ namespace ts { } function loadModuleFromNodeModulesFolder(extensions: Extensions, moduleName: string, nodeModulesFolder: string, nodeModulesFolderExists: boolean, failedLookupLocations: Push, state: ModuleResolutionState): Resolved | undefined { - const { packageName, rest } = getPackageName(moduleName); - const packageRootPath = combinePaths(nodeModulesFolder, packageName); - const { packageJsonContent, packageId } = getPackageJsonInfo(packageRootPath, rest, failedLookupLocations, !nodeModulesFolderExists, state); const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); + // First look for a nested package.json, as in `node_modules/foo/bar/package.json`. + let packageJsonContent: PackageJsonPathFields | undefined; + let packageId: PackageId | undefined; + const packageInfo = getPackageJsonInfo(candidate, "", failedLookupLocations, /*onlyRecordFailures*/ !nodeModulesFolderExists, state); + if (packageInfo.found) { + ({ packageJsonContent, packageId } = packageInfo); + } + else { + const { packageName, rest } = getPackageName(moduleName); + if (rest !== "") { // If "rest" is empty, we just did this search above. + const packageRootPath = combinePaths(nodeModulesFolder, packageName); + // Don't use a "types" or "main" from here because we're not loading the root, but a subdirectory -- just here for the packageId. + packageId = getPackageJsonInfo(packageRootPath, rest, failedLookupLocations, !nodeModulesFolderExists, state).packageId; + } + } const pathAndExtension = loadModuleFromFile(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state) || loadNodeModuleFromDirectoryWorker(extensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state, packageJsonContent); return withPackageId(packageId, pathAndExtension); diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.js b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.js new file mode 100644 index 00000000000..941774f4d35 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.js @@ -0,0 +1,20 @@ +//// [tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot.ts] //// + +//// [package.json] +// Loads from a "fake" nested package.json, not from the one at the root. + +{ "types": "types.d.ts" } + +//// [package.json] +{} + +//// [types.d.ts] +export const x: number; + +//// [a.ts] +import { x } from "foo/bar"; + + +//// [a.js] +"use strict"; +exports.__esModule = true; diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.symbols b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.symbols new file mode 100644 index 00000000000..57aaa9a96ef --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.symbols @@ -0,0 +1,8 @@ +=== /a.ts === +import { x } from "foo/bar"; +>x : Symbol(x, Decl(a.ts, 0, 8)) + +=== /node_modules/foo/bar/types.d.ts === +export const x: number; +>x : Symbol(x, Decl(types.d.ts, 0, 12)) + diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.trace.json b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.trace.json new file mode 100644 index 00000000000..bf5f0b0bbef --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.trace.json @@ -0,0 +1,14 @@ +[ + "======== Resolving module 'foo/bar' from '/a.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'foo/bar' from 'node_modules' folder, target file type 'TypeScript'.", + "Found 'package.json' at '/node_modules/foo/bar/package.json'.", + "File '/node_modules/foo/bar.ts' does not exist.", + "File '/node_modules/foo/bar.tsx' does not exist.", + "File '/node_modules/foo/bar.d.ts' does not exist.", + "'package.json' does not have a 'typings' field.", + "'package.json' has 'types' field 'types.d.ts' that references '/node_modules/foo/bar/types.d.ts'.", + "File '/node_modules/foo/bar/types.d.ts' exist - use it as a name resolution result.", + "Resolving real path for '/node_modules/foo/bar/types.d.ts', result '/node_modules/foo/bar/types.d.ts'.", + "======== Module name 'foo/bar' was successfully resolved to '/node_modules/foo/bar/types.d.ts'. ========" +] \ No newline at end of file diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.types b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.types new file mode 100644 index 00000000000..68a1585d768 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot.types @@ -0,0 +1,8 @@ +=== /a.ts === +import { x } from "foo/bar"; +>x : number + +=== /node_modules/foo/bar/types.d.ts === +export const x: number; +>x : number + diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.js b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.js new file mode 100644 index 00000000000..45398289241 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.js @@ -0,0 +1,20 @@ +//// [tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.ts] //// + +//// [package.json] +// Copy of `moduleResolution_packageJson_notAtPackageRoot` with `foo/@bar` instead of `foo/bar`. Should behave identically. + +{ "types": "types.d.ts" } + +//// [package.json] +{} + +//// [types.d.ts] +export const x: number; + +//// [a.ts] +import { x } from "foo/@bar"; + + +//// [a.js] +"use strict"; +exports.__esModule = true; diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.symbols b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.symbols new file mode 100644 index 00000000000..de21d67b629 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.symbols @@ -0,0 +1,8 @@ +=== /a.ts === +import { x } from "foo/@bar"; +>x : Symbol(x, Decl(a.ts, 0, 8)) + +=== /node_modules/foo/@bar/types.d.ts === +export const x: number; +>x : Symbol(x, Decl(types.d.ts, 0, 12)) + diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.trace.json b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.trace.json new file mode 100644 index 00000000000..72d413d0b2c --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.trace.json @@ -0,0 +1,14 @@ +[ + "======== Resolving module 'foo/@bar' from '/a.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'foo/@bar' from 'node_modules' folder, target file type 'TypeScript'.", + "Found 'package.json' at '/node_modules/foo/@bar/package.json'.", + "File '/node_modules/foo/@bar.ts' does not exist.", + "File '/node_modules/foo/@bar.tsx' does not exist.", + "File '/node_modules/foo/@bar.d.ts' does not exist.", + "'package.json' does not have a 'typings' field.", + "'package.json' has 'types' field 'types.d.ts' that references '/node_modules/foo/@bar/types.d.ts'.", + "File '/node_modules/foo/@bar/types.d.ts' exist - use it as a name resolution result.", + "Resolving real path for '/node_modules/foo/@bar/types.d.ts', result '/node_modules/foo/@bar/types.d.ts'.", + "======== Module name 'foo/@bar' was successfully resolved to '/node_modules/foo/@bar/types.d.ts'. ========" +] \ No newline at end of file diff --git a/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.types b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.types new file mode 100644 index 00000000000..6d8894ebce1 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.types @@ -0,0 +1,8 @@ +=== /a.ts === +import { x } from "foo/@bar"; +>x : number + +=== /node_modules/foo/@bar/types.d.ts === +export const x: number; +>x : number + diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.js b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.js new file mode 100644 index 00000000000..ed006b0e62d --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.js @@ -0,0 +1,18 @@ +//// [tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot.ts] //// + +//// [index.js] +not read + +//// [package.json] +{ "name": "foo", "version": "1.2.3", "types": "types.d.ts" } + +//// [types.d.ts] +export const x = 0; + +//// [a.ts] +import { x } from "foo/bar"; + + +//// [a.js] +"use strict"; +exports.__esModule = true; diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.symbols b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.symbols new file mode 100644 index 00000000000..c037bb343b0 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.symbols @@ -0,0 +1,4 @@ +=== /a.ts === +import { x } from "foo/bar"; +>x : Symbol(x, Decl(a.ts, 0, 8)) + diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.trace.json b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.trace.json new file mode 100644 index 00000000000..3a87769891b --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.trace.json @@ -0,0 +1,22 @@ +[ + "======== Resolving module 'foo/bar' from '/a.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'foo/bar' from 'node_modules' folder, target file type 'TypeScript'.", + "File '/node_modules/foo/bar/package.json' does not exist.", + "Found 'package.json' at '/node_modules/foo/package.json'.", + "File '/node_modules/foo/bar.ts' does not exist.", + "File '/node_modules/foo/bar.tsx' does not exist.", + "File '/node_modules/foo/bar.d.ts' does not exist.", + "File '/node_modules/foo/bar/index.ts' does not exist.", + "File '/node_modules/foo/bar/index.tsx' does not exist.", + "File '/node_modules/foo/bar/index.d.ts' does not exist.", + "Directory '/node_modules/@types' does not exist, skipping all lookups in it.", + "Loading module 'foo/bar' from 'node_modules' folder, target file type 'JavaScript'.", + "File '/node_modules/foo/bar/package.json' does not exist.", + "Found 'package.json' at '/node_modules/foo/package.json'.", + "File '/node_modules/foo/bar.js' does not exist.", + "File '/node_modules/foo/bar.jsx' does not exist.", + "File '/node_modules/foo/bar/index.js' exist - use it as a name resolution result.", + "Resolving real path for '/node_modules/foo/bar/index.js', result '/node_modules/foo/bar/index.js'.", + "======== Module name 'foo/bar' was successfully resolved to '/node_modules/foo/bar/index.js'. ========" +] \ No newline at end of file diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.types b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.types new file mode 100644 index 00000000000..460aa4b8f05 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot.types @@ -0,0 +1,4 @@ +=== /a.ts === +import { x } from "foo/bar"; +>x : any + diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.js b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.js new file mode 100644 index 00000000000..129feb6060f --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.js @@ -0,0 +1,20 @@ +//// [tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.ts] //// + +//// [index.js] +// Copy of `moduleResolution_packageJson_notAtPackageRoot` with `foo/@bar` instead of `foo/bar`. Should behave identically. + +not read + +//// [package.json] +{ "name": "foo", "version": "1.2.3", "types": "types.d.ts" } + +//// [types.d.ts] +export const x = 0; + +//// [a.ts] +import { x } from "foo/@bar"; + + +//// [a.js] +"use strict"; +exports.__esModule = true; diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.symbols b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.symbols new file mode 100644 index 00000000000..d1c3839b87b --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.symbols @@ -0,0 +1,4 @@ +=== /a.ts === +import { x } from "foo/@bar"; +>x : Symbol(x, Decl(a.ts, 0, 8)) + diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.trace.json b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.trace.json new file mode 100644 index 00000000000..c28189a1f7d --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.trace.json @@ -0,0 +1,22 @@ +[ + "======== Resolving module 'foo/@bar' from '/a.ts'. ========", + "Module resolution kind is not specified, using 'NodeJs'.", + "Loading module 'foo/@bar' from 'node_modules' folder, target file type 'TypeScript'.", + "File '/node_modules/foo/@bar/package.json' does not exist.", + "Found 'package.json' at '/node_modules/foo/package.json'.", + "File '/node_modules/foo/@bar.ts' does not exist.", + "File '/node_modules/foo/@bar.tsx' does not exist.", + "File '/node_modules/foo/@bar.d.ts' does not exist.", + "File '/node_modules/foo/@bar/index.ts' does not exist.", + "File '/node_modules/foo/@bar/index.tsx' does not exist.", + "File '/node_modules/foo/@bar/index.d.ts' does not exist.", + "Directory '/node_modules/@types' does not exist, skipping all lookups in it.", + "Loading module 'foo/@bar' from 'node_modules' folder, target file type 'JavaScript'.", + "File '/node_modules/foo/@bar/package.json' does not exist.", + "Found 'package.json' at '/node_modules/foo/package.json'.", + "File '/node_modules/foo/@bar.js' does not exist.", + "File '/node_modules/foo/@bar.jsx' does not exist.", + "File '/node_modules/foo/@bar/index.js' exist - use it as a name resolution result.", + "Resolving real path for '/node_modules/foo/@bar/index.js', result '/node_modules/foo/@bar/index.js'.", + "======== Module name 'foo/@bar' was successfully resolved to '/node_modules/foo/@bar/index.js'. ========" +] \ No newline at end of file diff --git a/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.types b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.types new file mode 100644 index 00000000000..47b12eea7a1 --- /dev/null +++ b/tests/baselines/reference/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.types @@ -0,0 +1,4 @@ +=== /a.ts === +import { x } from "foo/@bar"; +>x : any + diff --git a/tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot.ts b/tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot.ts new file mode 100644 index 00000000000..a93ea832b73 --- /dev/null +++ b/tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot.ts @@ -0,0 +1,16 @@ +// @noImplicitReferences: true +// @traceResolution: true + +// Loads from a "fake" nested package.json, not from the one at the root. + +// @Filename: /node_modules/foo/bar/package.json +{ "types": "types.d.ts" } + +// @Filename: /node_modules/foo/package.json +{} + +// @Filename: /node_modules/foo/bar/types.d.ts +export const x: number; + +// @Filename: /a.ts +import { x } from "foo/bar"; diff --git a/tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.ts b/tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.ts new file mode 100644 index 00000000000..55e3963357f --- /dev/null +++ b/tests/cases/compiler/moduleResolution_packageJson_notAtPackageRoot_fakeScopedPackage.ts @@ -0,0 +1,16 @@ +// @noImplicitReferences: true +// @traceResolution: true + +// Copy of `moduleResolution_packageJson_notAtPackageRoot` with `foo/@bar` instead of `foo/bar`. Should behave identically. + +// @Filename: /node_modules/foo/@bar/package.json +{ "types": "types.d.ts" } + +// @Filename: /node_modules/foo/package.json +{} + +// @Filename: /node_modules/foo/@bar/types.d.ts +export const x: number; + +// @Filename: /a.ts +import { x } from "foo/@bar"; diff --git a/tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot.ts b/tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot.ts new file mode 100644 index 00000000000..64e46c4e710 --- /dev/null +++ b/tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot.ts @@ -0,0 +1,14 @@ +// @noImplicitReferences: true +// @traceResolution: true + +// @Filename: /node_modules/foo/bar/index.js +not read + +// @Filename: /node_modules/foo/package.json +{ "name": "foo", "version": "1.2.3", "types": "types.d.ts" } + +// @Filename: /node_modules/foo/types.d.ts +export const x = 0; + +// @Filename: /a.ts +import { x } from "foo/bar"; diff --git a/tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.ts b/tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.ts new file mode 100644 index 00000000000..9b9f35b41e7 --- /dev/null +++ b/tests/cases/compiler/moduleResolution_packageJson_yesAtPackageRoot_fakeScopedPackage.ts @@ -0,0 +1,16 @@ +// @noImplicitReferences: true +// @traceResolution: true + +// Copy of `moduleResolution_packageJson_notAtPackageRoot` with `foo/@bar` instead of `foo/bar`. Should behave identically. + +// @Filename: /node_modules/foo/@bar/index.js +not read + +// @Filename: /node_modules/foo/package.json +{ "name": "foo", "version": "1.2.3", "types": "types.d.ts" } + +// @Filename: /node_modules/foo/types.d.ts +export const x = 0; + +// @Filename: /a.ts +import { x } from "foo/@bar";