Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1366,7 +1366,8 @@ namespace ts {
}

const resolvedModule = getResolvedModule(getSourceFileOfNode(location), moduleReference);
const sourceFile = resolvedModule && host.getSourceFile(resolvedModule.resolvedFileName);
const resolvedOrDiagnostic = resolvedModule && getResolutionOrDiagnostic(compilerOptions, resolvedModule);
const sourceFile = typeof resolvedOrDiagnostic === "string" && resolvedModule && host.getSourceFile(resolvedModule.resolvedFileName);
if (sourceFile) {
if (sourceFile.symbol) {
// merged symbol is module declaration symbol combined with all augmentations
Expand All @@ -1388,13 +1389,18 @@ namespace ts {

if (moduleNotFoundError) {
// report errors only if it was requested
const tsExtension = tryExtractTypeScriptExtension(moduleName);
if (tsExtension) {
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
error(errorNode, diag, tsExtension, removeExtension(moduleName, tsExtension));
if (resolvedOrDiagnostic && typeof resolvedOrDiagnostic !== "string") {
error(errorNode, resolvedOrDiagnostic.diag, moduleName, resolvedOrDiagnostic.file);
}
else {
error(errorNode, moduleNotFoundError, moduleName);
const tsExtension = tryExtractTypeScriptExtension(moduleName);
if (tsExtension) {
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
error(errorNode, diag, tsExtension, removeExtension(moduleName, tsExtension));
}
else {
error(errorNode, moduleNotFoundError, moduleName);
}
}
}
return undefined;
Expand Down
6 changes: 1 addition & 5 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1753,7 +1753,7 @@ namespace ts {
/** Must have ".d.ts" first because if ".ts" goes first, that will be detected as the extension instead of ".d.ts". */
export const supportedTypescriptExtensionsForExtractExtension = [".d.ts", ".ts", ".tsx"];
export const supportedJavascriptExtensions = [".js", ".jsx"];
const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);
export const allSupportedExtensions = supportedTypeScriptExtensions.concat(supportedJavascriptExtensions);

export function getSupportedExtensions(options?: CompilerOptions): string[] {
return options && options.allowJs ? allSupportedExtensions : supportedTypeScriptExtensions;
Expand Down Expand Up @@ -1850,10 +1850,6 @@ namespace ts {
return path.substring(0, path.length - extension.length);
}

export function isJsxOrTsxExtension(ext: string): boolean {
return ext === ".jsx" || ext === ".tsx";
}

export function changeExtension<T extends string | Path>(path: T, newExtension: string): T {
return <T>(removeFileExtension(path) + newExtension);
}
Expand Down
12 changes: 10 additions & 2 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2697,7 +2697,7 @@
"category": "Message",
"code": 6099
},
"'package.json' does not have 'types' field.": {
"'package.json' does not have a 'types' or 'main' field.": {
"category": "Message",
"code": 6100
},
Expand Down Expand Up @@ -2845,7 +2845,7 @@
"category": "Message",
"code": 6136
},
"No types specified in 'package.json' but 'allowJs' is set, so returning 'main' value of '{0}'": {
"No types specified in 'package.json', so returning 'main' value of '{0}'": {
"category": "Message",
"code": 6137
},
Expand All @@ -2865,6 +2865,14 @@
"category": "Message",
"code": 6141
},
"Module '{0}' was resolved to '{1}', but '--jsx' is not set.": {
"category": "Error",
"code": 6142
},
"Module '{0}' was resolved to '{1}', but '--allowJs' is not set.": {
"category": "Error",
"code": 6143
},
"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
"code": 7005
Expand Down
314 changes: 203 additions & 111 deletions src/compiler/moduleNameResolver.ts

Large diffs are not rendered by default.

65 changes: 46 additions & 19 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ namespace ts {
// - This calls resolveModuleNames, and then calls findSourceFile for each resolved module.
// As all these operations happen - and are nested - within the createProgram call, they close over the below variables.
// The current resolution depth is tracked by incrementing/decrementing as the depth first search progresses.
const maxNodeModulesJsDepth = typeof options.maxNodeModuleJsDepth === "number" ? options.maxNodeModuleJsDepth : 0;
const maxNodeModuleJsDepth = typeof options.maxNodeModuleJsDepth === "number" ? options.maxNodeModuleJsDepth : 0;
let currentNodeModulesDepth = 0;

// If a module has some of its imports skipped due to being at the depth limit under node_modules, then track
Expand All @@ -331,7 +331,7 @@ namespace ts {

let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModule[];
if (host.resolveModuleNames) {
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile);
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile).map(convertResolvedModuleFromHost);
}
else {
const loader = (moduleName: string, containingFile: string) => resolveModuleName(moduleName, containingFile, options, host).resolvedModule;
Expand Down Expand Up @@ -1164,7 +1164,7 @@ namespace ts {
}
// See if we need to reprocess the imports due to prior skipped imports
else if (file && modulesWithElidedImports[file.path]) {
if (currentNodeModulesDepth < maxNodeModulesJsDepth) {
if (currentNodeModulesDepth < maxNodeModuleJsDepth) {
modulesWithElidedImports[file.path] = false;
processImportedModules(file, getDirectoryPath(fileName));
}
Expand Down Expand Up @@ -1308,35 +1308,40 @@ namespace ts {
file.resolvedModules = createMap<ResolvedModule>();
const moduleNames = map(concatenate(file.imports, file.moduleAugmentations), getTextOfLiteral);
const resolutions = resolveModuleNamesWorker(moduleNames, getNormalizedAbsolutePath(file.fileName, currentDirectory));
Debug.assert(resolutions.length === moduleNames.length);
for (let i = 0; i < moduleNames.length; i++) {
const resolution = resolutions[i];
setResolvedModule(file, moduleNames[i], resolution);

// add file to program only if:
// - resolution was successful
// - noResolve is falsy
// - module name comes from the list of imports
// - it's not a top level JavaScript module that exceeded the search max
const isFromNodeModulesSearch = resolution && resolution.isExternalLibraryImport;
const isJsFileFromNodeModules = isFromNodeModulesSearch && hasJavaScriptFileExtension(resolution.resolvedFileName);
if (!resolution) {
continue;
}

const isFromNodeModulesSearch = resolution.isExternalLibraryImport;
const isJsFileFromNodeModules = !resolution.resolvedTsFileName;
const resolvedOrDiagnostic = getResolutionOrDiagnostic(options, resolution);
// Ignore it if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs') -- this will be reported by the checker, so just return undefined for now.
const resolvedFileName = typeof resolvedOrDiagnostic === "string" ? resolvedOrDiagnostic : undefined;

if (isFromNodeModulesSearch) {
currentNodeModulesDepth++;
}

const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModulesJsDepth;
const shouldAddFile = resolution && !options.noResolve && i < file.imports.length && !elideImport;
// add file to program only if:
// - resolution was successful
// - noResolve is falsy
// - module name comes from the list of imports
// - it's not a top level JavaScript module that exceeded the search max
const elideImport = isJsFileFromNodeModules && currentNodeModulesDepth > maxNodeModuleJsDepth;
const shouldAddFile = resolvedFileName && !options.noResolve && i < file.imports.length && !elideImport;

if (elideImport) {
modulesWithElidedImports[file.path] = true;
}
else if (shouldAddFile) {
findSourceFile(resolution.resolvedFileName,
toPath(resolution.resolvedFileName, currentDirectory, getCanonicalFileName),
/*isDefaultLib*/ false, /*isReference*/ false,
file,
skipTrivia(file.text, file.imports[i].pos),
file.imports[i].end);
const path = toPath(resolvedFileName, currentDirectory, getCanonicalFileName);
const pos = skipTrivia(file.text, file.imports[i].pos);
findSourceFile(resolvedFileName, path, /*isDefaultLib*/ false, /*isReference*/ false, file, pos, file.imports[i].end);
}

if (isFromNodeModulesSearch) {
Expand All @@ -1348,7 +1353,6 @@ namespace ts {
// no imports - drop cached module resolutions
file.resolvedModules = undefined;
}
return;
}

function computeCommonSourceDirectory(sourceFiles: SourceFile[]): string {
Expand Down Expand Up @@ -1573,4 +1577,27 @@ namespace ts {
programDiagnostics.add(createCompilerDiagnostic(message, emitFileName));
}
}

/* @internal */
/**
* Extracts the file name from a ResolvedModule, or returns a DiagnosticMessage if we are not set up to use that kind of file.
* The DiagnosticMessage's parameters are the imported module name, and the filename it resolved to.
*/
export function getResolutionOrDiagnostic(options: CompilerOptions, { resolvedTsFileName: ts, resolvedJsFileName: js }: ResolvedModule): string | { file: string, diag: DiagnosticMessage } {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit. i would rather we just use module.resolvedTsFileName || module.resolvedJsFileName for the module, and have this function just to report errors. this avoids the complexity of checking if the return type is string or not.

if (ts) {
return !options.jsx && fileExtensionIs(ts, ".tsx")
? { file: ts, diag: Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set }
: ts;
}
else {
if (!options.allowJs) {
return { file: js!, diag: Diagnostics.Module_0_was_resolved_to_1_but_allowJs_is_not_set };
}
else if (!options.jsx && fileExtensionIs(js!, ".jsx")) {
return { file: js!, diag: Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set };
}
else
return js!;
}
}
}
33 changes: 28 additions & 5 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3287,19 +3287,42 @@ namespace ts {
getDirectories?(path: string): string[];
}

/**
* Represents the result of module resolution.
* Module resolution will pick up tsx/jsx/js files even if '--jsx' and '--allowJs' are turned off.
* The Program will then filter results based on these flags.
*
* At least one of `resolvedTsFileName` or `resolvedJsFileName` must be defined,
* else resolution should just return `undefined` instead of a ResolvedModule.
*/
export interface ResolvedModule {
/**
* This should always be set to `resolvedTsFileName || resolvedJsFileName`.
* Present for backwards compatibility.
*/
resolvedFileName: string;
/*
* Denotes if 'resolvedFileName' is isExternalLibraryImport and thus should be proper external module:
/** TypeScript (.d.ts, .ts, .tsx) file that the module was resolved to. This will be preferred over a JS file. */
resolvedTsFileName: string | undefined;
/** JavaScript (or .jsx) file that the module was resolved to. This should be returned even if '--allowJs' (or '--jsx') is disabled. */
resolvedJsFileName: string | undefined;
/**
* Denotes if 'resolvedFileName' is isExternalLibraryImport and thus should be a proper external module:
* - be a .d.ts file
* - use top level imports\exports
* - don't use tripleslash references
*/
isExternalLibraryImport?: boolean;
isExternalLibraryImport: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should stay optional. otherwise we break other API

}

/**
* For backwards compatibility, a host may choose not to return `resolvedTsFileName` and `resolvedJsFileName` from a result ResolvedModule,
* in which case they will be inferred from the file extension.
* Prefer to return a full ResolvedModule.
*/
export type ResolvedModuleFromHost = { resolvedFileName: string; isExternalLibraryImport: boolean } | ResolvedModule;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i do not understand why we need this. the two new properties are optional, and we have logic to handle them being missing. so do not see the value here.


export interface ResolvedModuleWithFailedLookupLocations {
resolvedModule: ResolvedModule;
resolvedModule: ResolvedModule | undefined;
failedLookupLocations: string[];
}

Expand Down Expand Up @@ -3335,7 +3358,7 @@ namespace ts {
* If resolveModuleNames is implemented then implementation for members from ModuleResolutionHost can be just
* 'throw new Error("NotImplemented")'
*/
resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[];
resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModuleFromHost[];
/**
* This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files
*/
Expand Down
30 changes: 29 additions & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,29 @@ namespace ts {
sourceFile.resolvedModules[moduleNameText] = resolvedModule;
}

/** Host may have omitted resolvedTsFileName and resolvedJsFileName, in which case we should infer them from the file extension of resolvedFileName. */
export function convertResolvedModuleFromHost(resolved: ResolvedModuleFromHost | undefined): ResolvedModule | undefined {
if (resolved === undefined) {
return undefined;
}
// `resolvedTsFileName` and `resolvedJsFileName` should be present as properties even if undefined.
else if ("resolvedTsFileName" in resolved) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mm. do you really need that check? can we check when we create them instead?

const { resolvedFileName, resolvedTsFileName, resolvedJsFileName } = resolved as ResolvedModule;
Debug.assert(resolvedFileName === (resolvedTsFileName || resolvedJsFileName));
return resolved as ResolvedModule;
}
else {
const { resolvedFileName, isExternalLibraryImport } = resolved;
if (fileExtensionIsAny(resolvedFileName, supportedTypeScriptExtensions)) {
return { resolvedFileName, resolvedTsFileName: resolvedFileName, resolvedJsFileName: undefined, isExternalLibraryImport };
}
else {
Debug.assert(fileExtensionIsAny(resolvedFileName, supportedJavascriptExtensions));
return { resolvedFileName, resolvedTsFileName: undefined, resolvedJsFileName: resolvedFileName, isExternalLibraryImport };
}
}
}

export function setResolvedTypeReferenceDirective(sourceFile: SourceFile, typeReferenceDirectiveName: string, resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective): void {
if (!sourceFile.resolvedTypeReferenceDirectiveNames) {
sourceFile.resolvedTypeReferenceDirectiveNames = createMap<ResolvedTypeReferenceDirective>();
Expand All @@ -127,8 +150,13 @@ namespace ts {
}

/* @internal */
/**
* Considers two ResolvedModules equal if they have the same `resolvedFileName`.
* Thus `{ ts: foo, js: bar }` is equal to `{ ts: foo, js: baz }` because `ts` is preferred.
*/
export function moduleResolutionIsEqualTo(oldResolution: ResolvedModule, newResolution: ResolvedModule): boolean {
return oldResolution.resolvedFileName === newResolution.resolvedFileName && oldResolution.isExternalLibraryImport === newResolution.isExternalLibraryImport;
return oldResolution.isExternalLibraryImport === newResolution.isExternalLibraryImport &&
oldResolution.resolvedFileName === newResolution.resolvedFileName;
}

/* @internal */
Expand Down
Loading