Skip to content

Commit

Permalink
Add --allowNonJsExtensions, a flag for allowing arbitrary extension…
Browse files Browse the repository at this point in the history
…s on import paths
  • Loading branch information
weswigham committed Jan 4, 2023
1 parent f43cd0a commit b1fe76f
Show file tree
Hide file tree
Showing 117 changed files with 2,008 additions and 154 deletions.
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4738,7 +4738,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode);
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule, currentSourceFile);
const sourceFile = resolvedModule
&& (!resolutionDiagnostic || resolutionDiagnostic === Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set)
&& host.getSourceFile(resolvedModule.resolvedFileName);
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,14 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
description: Diagnostics.Enable_importing_json_files,
defaultValueDescription: false,
},
{
name: "allowNonJsExtensions",
type: "boolean",
affectsModuleResolution: true,
category: Diagnostics.Modules,
description: Diagnostics.Enable_importing_files_with_any_extension_provided_a_declaration_file_is_present,
defaultValueDescription: false,
},

{
name: "out",
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5190,6 +5190,18 @@
"category": "Message",
"code": 6261
},
"File name '{0}' has a '{1}' extension - looking up '{2}' instead.": {
"category": "Message",
"code": 6262
},
"Module '{0}' was resolved to '{1}', but '--allowNonJsExtensions' is not set.": {
"category": "Error",
"code": 6263
},
"Enable importing files with any extension, provided a declaration file is present.": {
"category": "Message",
"code": 6264
},

"Directory '{0}' has no containing package.json scope. Imports will not resolve.": {
"category": "Message",
Expand Down
83 changes: 40 additions & 43 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import {
getRelativePathFromDirectory,
getResolveJsonModule,
getRootLength,
hasJSFileExtension,
hasProperty,
hasTrailingDirectorySeparator,
hostGetCanonicalFileName,
Expand Down Expand Up @@ -99,7 +98,6 @@ import {
startsWith,
stringContains,
supportedDeclarationExtensions,
supportedTSExtensionsFlat,
supportedTSImplementationExtensions,
toPath,
tryExtractTSExtension,
Expand Down Expand Up @@ -151,7 +149,7 @@ function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | und
/** Result of trying to resolve a module. */
interface Resolved {
path: string;
extension: Extension;
extension: string;
packageId: PackageId | undefined;
/**
* When the resolved is not created from cache, the value is
Expand All @@ -170,7 +168,7 @@ interface Resolved {
interface PathAndExtension {
path: string;
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
ext: Extension;
ext: string;
resolvedUsingTsExtension: boolean | undefined;
}

Expand Down Expand Up @@ -1856,21 +1854,21 @@ function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecor
}

function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
// If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one;
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
if (hasJSFileExtension(candidate) ||
extensions & Extensions.Json && fileExtensionIs(candidate, Extension.Json) ||
extensions & (Extensions.TypeScript | Extensions.Declaration)
&& moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions)
&& fileExtensionIsOneOf(candidate, supportedTSExtensionsFlat)
) {
const extensionless = removeFileExtension(candidate);
const extension = candidate.substring(extensionless.length);
if (state.traceEnabled) {
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
}
return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state);
const filename = getBaseFileName(candidate);
if (filename.indexOf(".") === -1) {
return undefined; // extensionless import, no lookups performed, since we don't support extensionless files
}
let extensionless = removeFileExtension(candidate);
if (extensionless === candidate) {
// Once TS native extensions are handled, handle arbitrary extensions for declaration file mapping
extensionless = candidate.substring(0, candidate.lastIndexOf("."));
}

const extension = candidate.substring(extensionless.length);
if (state.traceEnabled) {
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
}
return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state);
}

/**
Expand Down Expand Up @@ -1909,47 +1907,46 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
case Extension.Mjs:
case Extension.Mts:
case Extension.Dmts:
return extensions & Extensions.TypeScript && tryExtension(Extension.Mts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dmts)
return extensions & Extensions.TypeScript && tryExtension(Extension.Mts, originalExtension === Extension.Mts || originalExtension === Extension.Dmts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dmts, originalExtension === Extension.Mts || originalExtension === Extension.Dmts)
|| extensions & Extensions.JavaScript && tryExtension(Extension.Mjs)
|| undefined;
case Extension.Cjs:
case Extension.Cts:
case Extension.Dcts:
return extensions & Extensions.TypeScript && tryExtension(Extension.Cts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dcts)
return extensions & Extensions.TypeScript && tryExtension(Extension.Cts, originalExtension === Extension.Cts || originalExtension === Extension.Dcts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dcts, originalExtension === Extension.Cts || originalExtension === Extension.Dcts)
|| extensions & Extensions.JavaScript && tryExtension(Extension.Cjs)
|| undefined;
case Extension.Json:
const originalCandidate = candidate;
if (extensions & Extensions.Declaration) {
candidate += Extension.Json;
const result = tryExtension(Extension.Dts);
if (result) return result;
}
if (extensions & Extensions.Json) {
candidate = originalCandidate;
const result = tryExtension(Extension.Json);
if (result) return result;
}
return undefined;
case Extension.Ts:
return extensions & Extensions.Declaration && tryExtension(".d.json.ts")
|| extensions & Extensions.Json && tryExtension(Extension.Json)
|| undefined;
case Extension.Tsx:
case Extension.Jsx:
// basically idendical to the ts/js case below, but prefers matching tsx and jsx files exactly before falling back to the ts or js file path
// (historically, we disallow having both a a.ts and a.tsx file in the same compilation, since their outputs clash)
// TODO: We should probably error if `"./a.tsx"` resolved to `"./a.ts"`, right?
return extensions & Extensions.TypeScript && (tryExtension(Extension.Tsx, originalExtension === Extension.Tsx) || tryExtension(Extension.Ts, originalExtension === Extension.Tsx))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts, originalExtension === Extension.Tsx)
|| extensions & Extensions.JavaScript && (tryExtension(Extension.Jsx) || tryExtension(Extension.Js))
|| undefined;
case Extension.Ts:
case Extension.Dts:
if (moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions) && extensionIsOk(extensions, originalExtension)) {
return tryExtension(originalExtension, /*resolvedUsingTsExtension*/ true);
}
// falls through
default:
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts) || tryExtension(Extension.Tsx))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts)
case Extension.Js:
case "":
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts, originalExtension === Extension.Ts || originalExtension === Extension.Dts) || tryExtension(Extension.Tsx, originalExtension === Extension.Ts || originalExtension === Extension.Dts))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts, originalExtension === Extension.Ts || originalExtension === Extension.Dts)
|| extensions & Extensions.JavaScript && (tryExtension(Extension.Js) || tryExtension(Extension.Jsx))
|| state.isConfigLookup && tryExtension(Extension.Json)
|| undefined;
default:
return extensions & Extensions.Declaration && !isDeclarationFileName(candidate + originalExtension) && tryExtension(`.d${originalExtension}.ts`)
|| undefined;

}

function tryExtension(ext: Extension, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
function tryExtension(ext: string, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
const path = tryFile(candidate + ext, onlyRecordFailures, state);
return path === undefined ? undefined : { path, ext, resolvedUsingTsExtension };
}
Expand Down
15 changes: 15 additions & 0 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
flatten,
forEach,
forEachAncestorDirectory,
getBaseFileName,
GetCanonicalFileName,
getDirectoryPath,
getEmitModuleResolutionKind,
Expand Down Expand Up @@ -85,6 +86,7 @@ import {
pathIsBareSpecifier,
pathIsRelative,
PropertyAccessExpression,
removeExtension,
removeFileExtension,
removeSuffix,
ResolutionMode,
Expand Down Expand Up @@ -1036,6 +1038,10 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
return noExtension + getJSExtensionForFile(fileName, options);
}
else if (!fileExtensionIsOneOf(fileName, [Extension.Dts]) && fileExtensionIsOneOf(fileName, [Extension.Ts]) && stringContains(fileName, ".d.")) {
// `foo.d.json.ts` and the like - remap back to `foo.json`
return tryGetRealFileNameForNonJsDeclarationFileName(fileName)!;
}

switch (allowedEndings[0]) {
case ModuleSpecifierEnding.Minimal:
Expand Down Expand Up @@ -1066,6 +1072,15 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
}
}

/** @internal */
export function tryGetRealFileNameForNonJsDeclarationFileName(fileName: string) {
const baseName = getBaseFileName(fileName);
if (!endsWith(fileName, Extension.Ts) || !stringContains(baseName, ".d.") || fileExtensionIsOneOf(baseName, [Extension.Dts])) return undefined;
const noExtension = removeExtension(fileName, Extension.Ts);
const ext = noExtension.substring(noExtension.lastIndexOf("."));
return noExtension.substring(0, noExtension.indexOf(".d.")) + ext;
}

function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension {
return tryGetJSExtensionForFile(fileName, options) ?? Debug.fail(`Extension ${extensionFromPath(fileName)} is unsupported:: FileName:: ${fileName}`);
}
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ import {
Expression,
ExpressionStatement,
ExpressionWithTypeArguments,
Extension,
ExternalModuleReference,
fileExtensionIs,
fileExtensionIsOneOf,
findIndex,
forEach,
Expand All @@ -98,6 +100,7 @@ import {
FunctionOrConstructorTypeNode,
FunctionTypeNode,
GetAccessorDeclaration,
getBaseFileName,
getBinaryOperatorPrecedence,
getFullWidth,
getJSDocCommentRanges,
Expand Down Expand Up @@ -328,6 +331,7 @@ import {
SpreadElement,
startsWith,
Statement,
stringContains,
StringLiteral,
supportedDeclarationExtensions,
SwitchStatement,
Expand Down Expand Up @@ -10124,7 +10128,7 @@ namespace IncrementalParser {

/** @internal */
export function isDeclarationFileName(fileName: string): boolean {
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions);
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions) || (fileExtensionIs(fileName, Extension.Ts) && stringContains(getBaseFileName(fileName), ".d."));;
}

function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ResolutionMode {
Expand Down
16 changes: 14 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3845,7 +3845,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
// Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs')
// This may still end up being an untyped module -- the file won't be included but imports will be allowed.
const shouldAddFile = resolvedFileName
&& !getResolutionDiagnostic(optionsForFile, resolution)
&& !getResolutionDiagnostic(optionsForFile, resolution, file)
&& !optionsForFile.noResolve
&& index < file.imports.length
&& !elideImport
Expand Down Expand Up @@ -4947,20 +4947,28 @@ export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePa
*
* @internal
*/
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull): DiagnosticMessage | undefined {
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull, { isDeclarationFile }: { isDeclarationFile: SourceFile["isDeclarationFile"] }): DiagnosticMessage | undefined {
switch (extension) {
case Extension.Ts:
case Extension.Dts:
case Extension.Mts:
case Extension.Dmts:
case Extension.Cts:
case Extension.Dcts:
// These are always allowed.
return undefined;
case Extension.Tsx:
return needJsx();
case Extension.Jsx:
return needJsx() || needAllowJs();
case Extension.Js:
case Extension.Mjs:
case Extension.Cjs:
return needAllowJs();
case Extension.Json:
return needResolveJsonModule();
default:
return needAllowNonJsExtensions();
}

function needJsx() {
Expand All @@ -4972,6 +4980,10 @@ export function getResolutionDiagnostic(options: CompilerOptions, { extension }:
function needResolveJsonModule() {
return getResolveJsonModule(options) ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used;
}
function needAllowNonJsExtensions() {
// But don't report the allowNonJsExtensions error from declaration files (no reason to report it, since the import doesn't have a runtime component)
return isDeclarationFile || options.allowNonJsExtensions ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_allowNonJsExtensions_is_not_set;
}
}

function getModuleNames({ imports, moduleAugmentations }: SourceFile): StringLiteralLike[] {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6974,6 +6974,7 @@ export interface CompilerOptions {
allowImportingTsExtensions?: boolean;
allowJs?: boolean;
/** @internal */ allowNonTsExtensions?: boolean;
allowNonJsExtensions?: boolean;
allowSyntheticDefaultImports?: boolean;
allowUmdGlobalAccess?: boolean;
allowUnreachableCode?: boolean;
Expand Down Expand Up @@ -7557,7 +7558,7 @@ export interface ResolvedModuleFull extends ResolvedModule {
* Extension of resolvedFileName. This must match what's at the end of resolvedFileName.
* This is optional for backwards-compatibility, but will be added if not provided.
*/
extension: Extension;
extension: string;
packageId?: PackageId;
}

Expand Down
11 changes: 6 additions & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {
EmitResolver,
EmitTextWriter,
emptyArray,
endsWith,
ensurePathIsNonModuleName,
ensureTrailingDirectorySeparator,
EntityName,
Expand Down Expand Up @@ -5499,7 +5500,7 @@ export function getDeclarationEmitOutputFilePathWorker(fileName: string, options
export function getDeclarationEmitExtensionForPath(path: string) {
return fileExtensionIsOneOf(path, [Extension.Mjs, Extension.Mts]) ? Extension.Dmts :
fileExtensionIsOneOf(path, [Extension.Cjs, Extension.Cts]) ? Extension.Dcts :
fileExtensionIsOneOf(path, [Extension.Json]) ? `.json.d.ts` : // Drive-by redefinition of json declaration file output name so if it's ever enabled, it behaves well
fileExtensionIsOneOf(path, [Extension.Json]) ? `.d.json.ts` : // Drive-by redefinition of json declaration file output name so if it's ever enabled, it behaves well
Extension.Dts;
}

Expand All @@ -5511,7 +5512,7 @@ export function getDeclarationEmitExtensionForPath(path: string) {
export function getPossibleOriginalInputExtensionForExtension(path: string) {
return fileExtensionIsOneOf(path, [Extension.Dmts, Extension.Mjs, Extension.Mts]) ? [Extension.Mts, Extension.Mjs] :
fileExtensionIsOneOf(path, [Extension.Dcts, Extension.Cjs, Extension.Cts]) ? [Extension.Cts, Extension.Cjs]:
fileExtensionIsOneOf(path, [`.json.d.ts`]) ? [Extension.Json] :
fileExtensionIsOneOf(path, [`.d.json.ts`]) ? [Extension.Json] :
[Extension.Tsx, Extension.Ts, Extension.Jsx, Extension.Js];
}

Expand Down Expand Up @@ -8672,12 +8673,12 @@ export function positionIsSynthesized(pos: number): boolean {
*
* @internal
*/
export function extensionIsTS(ext: Extension): boolean {
return ext === Extension.Ts || ext === Extension.Tsx || ext === Extension.Dts || ext === Extension.Cts || ext === Extension.Mts || ext === Extension.Dmts || ext === Extension.Dcts;
export function extensionIsTS(ext: string): boolean {
return ext === Extension.Ts || ext === Extension.Tsx || ext === Extension.Dts || ext === Extension.Cts || ext === Extension.Mts || ext === Extension.Dmts || ext === Extension.Dcts || (startsWith(ext, ".d.") && endsWith(ext, ".ts"));
}

/** @internal */
export function resolutionExtensionIsTSOrJson(ext: Extension) {
export function resolutionExtensionIsTSOrJson(ext: string) {
return extensionIsTS(ext) || ext === Extension.Json;
}

Expand Down
2 changes: 1 addition & 1 deletion src/harness/compilerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export class CompilationResult {
}
else {
path = vpath.resolve(this.vfs.cwd(), path);
const outDir = ext === ".d.ts" || ext === ".json.d.ts" || ext === ".d.mts" || ext === ".d.cts" ? this.options.declarationDir || this.options.outDir : this.options.outDir;
const outDir = ext === ".d.ts" || ext === ".d.mts" || ext === ".d.cts" || (ext.endsWith(".ts") || ts.stringContains(ext, ".d.")) ? this.options.declarationDir || this.options.outDir : this.options.outDir;
if (outDir) {
const common = this.commonSourceDirectory;
if (common) {
Expand Down
Loading

0 comments on commit b1fe76f

Please sign in to comment.