diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 5bbd74d6f47f4..d4ddffa2664e7 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -299,6 +299,11 @@ namespace ts { name: "noImplicitUseStrict", type: "boolean", description: Diagnostics.Do_not_emit_use_strict_directives_in_module_output + }, + { + name: "maxNodeSearchJsDepth", + type: "number", + description: Diagnostics.The_maximum_depth_of_JavaScript_modules_to_load_by_searching_node_modules } ]; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 1a8a53d517c10..091a69491ae18 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2171,7 +2171,7 @@ "category": "Error", "code": 5059 }, - + "Concatenate and emit output to single file.": { "category": "Message", @@ -2450,6 +2450,10 @@ "category": "Message", "code": 6112 }, + "The maximum depth of JavaScript modules to load by searching node_modules": { + "category": "Message", + "code": 6085 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 3c75f31add2a4..230547e878223 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -65,7 +65,7 @@ namespace ts { : { resolvedModule: undefined, failedLookupLocations }; } else { - return loadModuleFromNodeModules(moduleName, containingDirectory, host); + return loadModuleFromNodeModules(moduleName, containingDirectory, host, compilerOptions); } } @@ -77,7 +77,7 @@ namespace ts { /** * @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary - * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. + * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. */ function loadNodeModuleFromFile(extensions: string[], candidate: string, failedLookupLocation: string[], onlyRecordFailures: boolean, host: ModuleResolutionHost): string { return forEach(extensions, tryLoad); @@ -99,19 +99,27 @@ namespace ts { const directoryExists = !onlyRecordFailures && directoryProbablyExists(candidate, host); if (directoryExists && host.fileExists(packageJsonPath)) { - let jsonContent: { typings?: string }; + let jsonContent: { typings?: string; main?: string }; try { const jsonText = host.readFile(packageJsonPath); - jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined }; + jsonContent = jsonText ? <{ typings?: string; main?: string }>JSON.parse(jsonText) : { typings: undefined }; } catch (e) { - // gracefully handle if readFile fails or returns not JSON + // gracefully handle if readFile fails or returns not JSON jsonContent = { typings: undefined }; } if (typeof jsonContent.typings === "string") { const path = normalizePath(combinePaths(candidate, jsonContent.typings)); + const result = loadNodeModuleFromFile(/*don't add extension*/[""], path, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(path), host), host); + if (result) { + return result; + } + } + if (typeof jsonContent.main === "string") { + // If 'main' points to 'foo.js', we still want to try and load 'foo.d.ts' and 'foo.ts' first (and only 'foo.js' if 'allowJs' is set). + const path = normalizePath(combinePaths(candidate, removeFileExtension(jsonContent.main))); const result = loadNodeModuleFromFile(extensions, path, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(path), host), host); if (result) { return result; @@ -126,7 +134,7 @@ namespace ts { return loadNodeModuleFromFile(extensions, combinePaths(candidate, "index"), failedLookupLocation, !directoryExists, host); } - function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { + function loadModuleFromNodeModules(moduleName: string, directory: string, host: ModuleResolutionHost, compilerOptions: CompilerOptions): ResolvedModuleWithFailedLookupLocations { const failedLookupLocations: string[] = []; directory = normalizeSlashes(directory); while (true) { @@ -135,13 +143,14 @@ namespace ts { const nodeModulesFolder = combinePaths(directory, "node_modules"); const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, host); const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); - // Load only typescript files irrespective of allowJs option if loading from node modules - let result = loadNodeModuleFromFile(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, host); + + const supportedExtensions = getSupportedExtensions(compilerOptions); + let result = loadNodeModuleFromFile(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, host); if (result) { return { resolvedModule: { resolvedFileName: result, isExternalLibraryImport: true }, failedLookupLocations }; } - result = loadNodeModuleFromDirectory(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, host); + result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, host); if (result) { return { resolvedModule: { resolvedFileName: result, isExternalLibraryImport: true }, failedLookupLocations }; } @@ -181,7 +190,7 @@ namespace ts { searchName = normalizePath(combinePaths(searchPath, moduleName)); referencedSourceFile = forEach(supportedExtensions, extension => { if (extension === ".tsx" && !compilerOptions.jsx) { - // resolve .tsx files only if jsx support is enabled + // resolve .tsx files only if jsx support is enabled // 'logical not' handles both undefined and None cases return undefined; } @@ -382,7 +391,7 @@ namespace ts { const filesByName = createFileMap(); // stores 'filename -> file association' ignoring case - // used to track cases when two file names differ only in casing + // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap(fileName => fileName.toLowerCase()) : undefined; if (oldProgram) { @@ -960,7 +969,7 @@ namespace ts { } // TypeScript 1.0 spec (April 2014): 12.1.6 - // An ExternalImportDeclaration in an AmbientExternalModuleDeclaration may reference other external modules + // An ExternalImportDeclaration in an AmbientExternalModuleDeclaration may reference other external modules // only through top - level external module names. Relative external module names are not permitted. if (!inAmbientModule || !isExternalModuleNameRelative((moduleNameExpr).text)) { (imports || (imports = [])).push(moduleNameExpr); @@ -978,7 +987,7 @@ namespace ts { (moduleAugmentations || (moduleAugmentations = [])).push(moduleName); } else if (!inAmbientModule) { - // An AmbientExternalModuleDeclaration declares an external module. + // An AmbientExternalModuleDeclaration declares an external module. // This type of declaration is permitted only in the global module. // The StringLiteral must specify a top - level external module name. // Relative external module names are not permitted @@ -1055,7 +1064,7 @@ namespace ts { } // Get source file from normalized fileName - function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number): SourceFile { + function findSourceFile(fileName: string, path: Path, isDefaultLib: boolean, refFile?: SourceFile, refPos?: number, refEnd?: number, isFileFromNodeSearch?: boolean): SourceFile { if (filesByName.contains(path)) { const file = filesByName.get(path); // try to check if we've already seen this file but with a different casing in path @@ -1064,6 +1073,13 @@ namespace ts { reportFileNamesDifferOnlyInCasingError(fileName, file.fileName, refFile, refPos, refEnd); } + // If this was a file found by a node_modules search, set the nodeModuleSearchDistance to parent distance + 1. + if (isFileFromNodeSearch) { + const newDistance = (refFile && refFile.nodeModuleSearchDistance) === undefined ? 1 : refFile.nodeModuleSearchDistance + 1; + // If already set on the file, don't overwrite if it was already found closer (which may be '0' if added as a root file) + file.nodeModuleSearchDistance = (typeof file.nodeModuleSearchDistance === "number") ? Math.min(file.nodeModuleSearchDistance, newDistance) : newDistance; + } + return file; } @@ -1082,6 +1098,12 @@ namespace ts { if (file) { file.path = path; + // Default to same distance as parent. Add one if found by a search. + file.nodeModuleSearchDistance = (refFile && refFile.nodeModuleSearchDistance) || 0; + if (isFileFromNodeSearch) { + file.nodeModuleSearchDistance++; + } + if (host.useCaseSensitiveFileNames()) { // for case-sensitive file systems check if we've already seen some file with similar filename ignoring case const existingFile = filesByNameIgnoreCase.get(path); @@ -1126,11 +1148,13 @@ namespace ts { } function processImportedModules(file: SourceFile, basePath: string) { + const maxJsNodeModuleSearchDistance = options.maxNodeSearchJsDepth || 0; collectExternalModuleReferences(file); if (file.imports.length || file.moduleAugmentations.length) { file.resolvedModules = {}; const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral); const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory)); + file.nodeModuleSearchDistance = file.nodeModuleSearchDistance || 0; for (let i = 0; i < moduleNames.length; i++) { const resolution = resolutions[i]; setResolvedModule(file, moduleNames[i], resolution); @@ -1138,16 +1162,22 @@ namespace ts { // - resolution was successfull // - noResolve is falsy // - module name come from the list fo imports - const shouldAddFile = resolution && - !options.noResolve && - i < file.imports.length; + // - it's not a top level JavaScript module that exceeded the search max + const exceedsJsSearchDepth = resolution && resolution.isExternalLibraryImport && + hasJavaScriptFileExtension(resolution.resolvedFileName) && + file.nodeModuleSearchDistance >= maxJsNodeModuleSearchDistance; + const shouldAddFile = resolution && !options.noResolve && i < file.imports.length && !exceedsJsSearchDepth; if (shouldAddFile) { - const importedFile = findSourceFile(resolution.resolvedFileName, toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), /*isDefaultLib*/ false, file, skipTrivia(file.text, file.imports[i].pos), file.imports[i].end); - - if (importedFile && resolution.isExternalLibraryImport) { - // Since currently irrespective of allowJs, we only look for supportedTypeScript extension external module files, - // this check is ok. Otherwise this would be never true for javascript file + const importedFile = findSourceFile(resolution.resolvedFileName, + toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName), + /*isDefaultLib*/ false, + file, + skipTrivia(file.text, file.imports[i].pos), + file.imports[i].end, + resolution.isExternalLibraryImport); + + if (importedFile && resolution.isExternalLibraryImport && fileExtensionIs(importedFile.fileName, ".ts")) { if (!isExternalModule(importedFile) && importedFile.statements.length) { const start = getTokenPosOfNode(file.imports[i], file); fileProcessingDiagnostics.add(createFileDiagnostic(file, start, file.imports[i].end - start, Diagnostics.Exported_external_package_typings_file_0_is_not_a_module_Please_contact_the_package_author_to_update_the_package_definition, importedFile.fileName)); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 31ff055f986a2..47ef7d94f3309 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1565,6 +1565,8 @@ namespace ts { /* @internal */ externalModuleIndicator: Node; // The first node that causes this file to be a CommonJS module /* @internal */ commonJsModuleIndicator: Node; + // The number of times node_modules was searched to locate the package containing this file + /* @internal */ nodeModuleSearchDistance?: number; /* @internal */ identifiers: Map; /* @internal */ nodeCount: number; @@ -2436,6 +2438,7 @@ namespace ts { allowSyntheticDefaultImports?: boolean; allowJs?: boolean; noImplicitUseStrict?: boolean; + maxNodeSearchJsDepth?: number; /* @internal */ stripInternal?: boolean; // Skip checking lib.d.ts to help speed up tests. diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index d93b37ce1add0..e79932f5a28f0 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2033,7 +2033,7 @@ namespace ts { else { const sourceFiles = targetSourceFile === undefined ? host.getSourceFiles() : [targetSourceFile]; for (const sourceFile of sourceFiles) { - if (!isDeclarationFile(sourceFile)) { + if (!isDeclarationFile(sourceFile) && !sourceFile.nodeModuleSearchDistance) { onSingleFileEmit(host, sourceFile); } } @@ -2068,6 +2068,7 @@ namespace ts { // Can emit only sources that are not declaration file and are either non module code or module with --module or --target es6 specified const bundledSources = filter(host.getSourceFiles(), sourceFile => !isDeclarationFile(sourceFile) && // Not a declaration file + !sourceFile.nodeModuleSearchDistance && // Not loaded from searching under node_modules (!isExternalModule(sourceFile) || // non module file (getEmitModuleKind(options) && isExternalModule(sourceFile)))); // module that can emit - note falsy value from getEmitModuleKind means the module kind that shouldn't be emitted if (bundledSources.length) { diff --git a/tests/baselines/reference/nodeResolution6.js b/tests/baselines/reference/nodeResolution6.js index 58a9b907250d2..196e8ae57cf54 100644 --- a/tests/baselines/reference/nodeResolution6.js +++ b/tests/baselines/reference/nodeResolution6.js @@ -13,7 +13,5 @@ export declare var y; import y = require("a"); -//// [ref.js] -var x = 1; //// [b.js] "use strict"; diff --git a/tests/baselines/reference/nodeResolution8.js b/tests/baselines/reference/nodeResolution8.js index 36b53eec553ef..1d90399ff706e 100644 --- a/tests/baselines/reference/nodeResolution8.js +++ b/tests/baselines/reference/nodeResolution8.js @@ -12,7 +12,5 @@ export declare var y; //// [b.ts] import y = require("a"); -//// [ref.js] -var x = 1; //// [b.js] "use strict"; diff --git a/tests/cases/unittests/moduleResolution.ts b/tests/cases/unittests/moduleResolution.ts index 81d7c5f8b2e2f..a29d7f445a55d 100644 --- a/tests/cases/unittests/moduleResolution.ts +++ b/tests/cases/unittests/moduleResolution.ts @@ -179,6 +179,9 @@ module ts { "/a/b/foo.ts", "/a/b/foo.tsx", "/a/b/foo.d.ts", + "/c/d.ts", + "/c/d.tsx", + "/c/d.d.ts", "/a/b/foo/index.ts", "/a/b/foo/index.tsx", ]);