diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 86d073f7d4..9a4a96f373 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -326,6 +326,11 @@ namespace ts { name: "noImplicitUseStrict", type: "boolean", description: Diagnostics.Do_not_emit_use_strict_directives_in_module_output + }, + { + name: "maxNodeModuleJsDepth", + type: "number", + description: Diagnostics.The_maximum_dependency_depth_to_search_under_node_modules_and_load_JavaScript_files } ]; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 25b86e67d3..d7b5674680 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2576,6 +2576,10 @@ "category": "Message", "code": 6112 }, + "The maximum dependency depth to search under node_modules and load JavaScript files": { + "category": "Message", + "code": 6113 + }, "Variable '{0}' implicitly has an '{1}' type.": { "category": "Error", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 7a2e65ed33..08d7a553b5 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -9,10 +9,10 @@ namespace ts { /* @internal */ export let ioWriteTime = 0; /** The version of the TypeScript compiler release */ + export const version = "1.9.0"; const emptyArray: any[] = []; - - export const version = "1.9.0"; + const startsWithDotSlashOrDotDotSlash = /^(\.\/|\.\.\/)/; export function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean): string { let fileName = "tsconfig.json"; @@ -79,9 +79,7 @@ namespace ts { return false; } - const i = moduleName.lastIndexOf("./", 1); - const startsWithDotSlashOrDotDotSlash = i === 0 || (i === 1 && moduleName.charCodeAt(0) === CharacterCodes.dot); - return !startsWithDotSlashOrDotDotSlash; + return !startsWithDotSlashOrDotDotSlash.test(moduleName); } interface ModuleResolutionState { @@ -448,11 +446,11 @@ namespace ts { trace(state.host, Diagnostics.Found_package_json_at_0, packageJsonPath); } - let jsonContent: { typings?: string }; + let jsonContent: { typings?: string; main?: string }; try { const jsonText = state.host.readFile(packageJsonPath); - jsonContent = jsonText ? <{ typings?: string }>JSON.parse(jsonText) : { typings: undefined }; + jsonContent = jsonText ? <{ typings?: string; main?: string }>JSON.parse(jsonText) : { typings: undefined, main: undefined }; } catch (e) { // gracefully handle if readFile fails or returns not JSON @@ -465,7 +463,7 @@ namespace ts { if (state.traceEnabled) { trace(state.host, Diagnostics.package_json_has_typings_field_0_that_references_1, jsonContent.typings, typingsFile); } - const result = loadModuleFromFile(typingsFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state); + const result = loadModuleFromFile(typingsFile, /* don't add extension */ [""], failedLookupLocation, !directoryProbablyExists(getDirectoryPath(typingsFile), state.host), state); if (result) { return result; } @@ -479,6 +477,15 @@ namespace ts { trace(state.host, Diagnostics.package_json_does_not_have_typings_field); } } + // TODO (billti): tracing as per above + 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 mainFile = normalizePath(combinePaths(candidate, removeFileExtension(jsonContent.main))); + const result = loadModuleFromFile(mainFile, extensions, failedLookupLocation, !directoryProbablyExists(getDirectoryPath(mainFile), state.host), state); + if (result) { + return result; + } + } } else { if (state.traceEnabled) { @@ -499,12 +506,13 @@ namespace ts { const nodeModulesFolder = combinePaths(directory, "node_modules"); const nodeModulesFolderExists = directoryProbablyExists(nodeModulesFolder, state.host); const candidate = normalizePath(combinePaths(nodeModulesFolder, moduleName)); - // Load only typescript files irrespective of allowJs option if loading from node modules - let result = loadModuleFromFile(candidate, supportedTypeScriptExtensions, failedLookupLocations, !nodeModulesFolderExists, state); + + const supportedExtensions = getSupportedExtensions(state.compilerOptions); + let result = loadModuleFromFile(candidate, supportedExtensions, failedLookupLocations, !nodeModulesFolderExists, state); if (result) { return result; } - result = loadNodeModuleFromDirectory(supportedTypeScriptExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state); + result = loadNodeModuleFromDirectory(supportedExtensions, candidate, failedLookupLocations, !nodeModulesFolderExists, state); if (result) { return result; } @@ -1397,7 +1405,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 @@ -1406,6 +1414,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; } @@ -1424,6 +1439,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); @@ -1468,11 +1489,13 @@ namespace ts { } function processImportedModules(file: SourceFile, basePath: string) { + const maxJsNodeModuleSearchDistance = options.maxNodeModuleJsDepth || 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); @@ -1480,16 +1503,23 @@ namespace ts { // - resolution was successful // - 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); + 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) { - // 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 + // TODO (billti): Should we check here if a JavaScript file is a CommonJS file, or doesn't have /// references? + if (importedFile && resolution.isExternalLibraryImport && !hasJavaScriptFileExtension(importedFile.fileName)) { 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 6b9f49cac0..b414ad50aa 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1536,6 +1536,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; @@ -2419,6 +2421,7 @@ namespace ts { traceModuleResolution?: boolean; allowSyntheticDefaultImports?: boolean; allowJs?: boolean; + maxNodeModuleJsDepth?: number; noImplicitUseStrict?: boolean; /* @internal */ stripInternal?: boolean; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5494db5fbd..7a44307368 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2035,7 +2035,8 @@ namespace ts { else { const sourceFiles = targetSourceFile === undefined ? host.getSourceFiles() : [targetSourceFile]; for (const sourceFile of sourceFiles) { - if (!isDeclarationFile(sourceFile)) { + // Don't emit if source file is a declaration file, or was found by a search under 'node_modules' + if (!isDeclarationFile(sourceFile) && !sourceFile.nodeModuleSearchDistance) { onSingleFileEmit(host, sourceFile); } } @@ -2069,9 +2070,10 @@ namespace ts { function onBundledEmit(host: EmitHost) { // 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 - (!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 + 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) { const jsFilePath = options.outFile || options.out; const emitFileNames: EmitFileNames = {