--moduleResolution bundler (formerly known as hybrid) (#51669)

* WIP

* Add extension error back unless noEmit is set

* Add non-relative tests

* Add error for importing from declaration file

* Update unit test

* Add explicit flag for importing from .ts extensions

* Add module specifier resolution changes

* Add auto-import tests

* Disallow relative imports into node_modules

* Ensure auto-imports don’t suggest ./node_modules;

* Test a non-portable declaration emit issue

* Test auto-importing TSX file

* Update path completions

* Fix lint due to merge

* Remove minimal-specific stuff

* Remove minimal tests

* Update unit tests

* Add options

* Add customConditions option

* Add first tests

* CJS constructs are not allowed

* Add another test

* Fix extension adding/replacing priority

* Update test to reflect the choice not to block on unrecognized extensions

* Add auto-imports and string completions tests

* Revamp string completions ending preferences

* Comment test

* Auto-imports of declaration files cannot use .ts extension

* Have declaration file auto imports default to extensionless instead

* Add test for custom conditions

* Fix indentation

* Add baseline showing resolvePackageJsonImports/Exports compatibility

* Fix test and prevent CJS require from resolving

* Update unit test baselines

* Fix bad merge conflict resolution

* Make resolvedUsingTsExtension optional

* Update missed baselines

* Revert now-unnecessary API implementation changes

* Clean up

* Update baselines to es5 emit

* Rename to `bundler`
This commit is contained in:
Andrew Branch
2022-12-13 13:35:16 -08:00
committed by GitHub
parent ad354c2d75
commit a5dde88dce
135 changed files with 4077 additions and 481 deletions

View File

@@ -2,6 +2,7 @@ import {
append,
appendIfUnique,
arrayFrom,
arrayIsEqualTo,
changeAnyExtension,
CharacterCodes,
combinePaths,
@@ -9,10 +10,12 @@ import {
comparePaths,
Comparison,
CompilerOptions,
concatenate,
contains,
containsPath,
createCompilerDiagnostic,
Debug,
deduplicate,
Diagnostic,
DiagnosticMessage,
DiagnosticReporter,
@@ -47,12 +50,14 @@ import {
getPathsBasePath,
getPossibleOriginalInputExtensionForExtension,
getRelativePathFromDirectory,
getResolveJsonModule,
getRootLength,
hasJSFileExtension,
hasProperty,
hasTrailingDirectorySeparator,
hostGetCanonicalFileName,
isArray,
isDeclarationFileName,
isExternalModuleNameRelative,
isRootedDiskPath,
isString,
@@ -94,6 +99,7 @@ import {
startsWith,
stringContains,
supportedDeclarationExtensions,
supportedTSExtensionsFlat,
supportedTSImplementationExtensions,
toPath,
tryExtractTSExtension,
@@ -128,7 +134,7 @@ function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExten
};
}
}
return r && { path: r.path, extension: r.ext, packageId };
return r && { path: r.path, extension: r.ext, packageId, resolvedUsingTsExtension: r.resolvedUsingTsExtension };
}
function noPackageId(r: PathAndExtension | undefined): Resolved | undefined {
@@ -138,7 +144,7 @@ function noPackageId(r: PathAndExtension | undefined): Resolved | undefined {
function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined {
if (r) {
Debug.assert(r.packageId === undefined);
return { path: r.path, ext: r.extension };
return { path: r.path, ext: r.extension, resolvedUsingTsExtension: r.resolvedUsingTsExtension };
}
}
@@ -157,6 +163,7 @@ interface Resolved {
* Note: This is a file name with preserved original casing, not a normalized `Path`.
*/
originalPath?: string | true;
resolvedUsingTsExtension: boolean | undefined;
}
/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */
@@ -164,6 +171,7 @@ interface PathAndExtension {
path: string;
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
ext: Extension;
resolvedUsingTsExtension: boolean | undefined;
}
/**
@@ -215,7 +223,14 @@ function createResolvedModuleWithFailedLookupLocations(
return resultFromCache;
}
return {
resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId },
resolvedModule: resolved && {
resolvedFileName: resolved.path,
originalPath: resolved.originalPath === true ? undefined : resolved.originalPath,
extension: resolved.extension,
isExternalLibraryImport,
packageId: resolved.packageId,
resolvedUsingTsExtension: !!resolved.resolvedUsingTsExtension,
},
failedLookupLocations: initializeResolutionField(failedLookupLocations),
affectingLocations: initializeResolutionField(affectingLocations),
resolutionDiagnostics: initializeResolutionField(diagnostics),
@@ -488,7 +503,7 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
const failedLookupLocations: string[] = [];
const affectingLocations: string[] = [];
let features = getDefaultNodeResolutionFeatures(options);
let features = getNodeResolutionFeatures(options);
// Unlike `import` statements, whose mode-calculating APIs are all guaranteed to return `undefined` if we're in an un-mode-ed module resolution
// setting, type references will return their target mode regardless of options because of how the parser works, so we guard against the mode being
// set in a non-modal module resolution setting here. Do note that our behavior is not particularly well defined when these mode-overriding imports
@@ -499,7 +514,7 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
if (resolutionMode === ModuleKind.ESNext && (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node16 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext)) {
features |= NodeResolutionFeatures.EsmMode;
}
const conditions = features & NodeResolutionFeatures.Exports ? features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] : [];
const conditions = features & NodeResolutionFeatures.Exports ? getConditions(options, !!(features & NodeResolutionFeatures.EsmMode)) : [];
const diagnostics: Diagnostic[] = [];
const moduleResolutionState: ModuleResolutionState = {
compilerOptions: options,
@@ -614,10 +629,44 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
}
}
function getDefaultNodeResolutionFeatures(options: CompilerOptions) {
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node16 ? NodeResolutionFeatures.Node16Default :
getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault :
NodeResolutionFeatures.None;
function getNodeResolutionFeatures(options: CompilerOptions) {
let features = NodeResolutionFeatures.None;
switch (getEmitModuleResolutionKind(options)) {
case ModuleResolutionKind.Node16:
features = NodeResolutionFeatures.Node16Default;
break;
case ModuleResolutionKind.NodeNext:
features = NodeResolutionFeatures.NodeNextDefault;
break;
case ModuleResolutionKind.Bundler:
features = NodeResolutionFeatures.BundlerDefault;
break;
}
if (options.resolvePackageJsonExports) {
features |= NodeResolutionFeatures.Exports;
}
else if (options.resolvePackageJsonExports === false) {
features &= ~NodeResolutionFeatures.Exports;
}
if (options.resolvePackageJsonImports) {
features |= NodeResolutionFeatures.Imports;
}
else if (options.resolvePackageJsonImports === false) {
features &= ~NodeResolutionFeatures.Imports;
}
return features;
}
function getConditions(options: CompilerOptions, esmMode?: boolean) {
// conditions are only used by the node16/nodenext/bundler resolvers - there's no priority order in the list,
// it's essentially a set (priority is determined by object insertion order in the object we look at).
const conditions = esmMode || getEmitModuleResolutionKind(options) === ModuleResolutionKind.Bundler
? ["node", "import"]
: ["node", "require"];
if (!options.noDtsResolution) {
conditions.push("types");
}
return concatenate(conditions, options.customConditions);
}
/**
@@ -1235,6 +1284,9 @@ export function resolveModuleName(moduleName: string, containingFile: string, co
case ModuleResolutionKind.Classic:
result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
break;
case ModuleResolutionKind.Bundler:
result = bundlerModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
break;
default:
return Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`);
}
@@ -1489,6 +1541,8 @@ export enum NodeResolutionFeatures {
NodeNextDefault = AllFeatures,
BundlerDefault = Imports | SelfName | Exports | ExportsPatternTrailers,
EsmMode = 1 << 5,
}
@@ -1528,7 +1582,7 @@ function nodeNextModuleNameResolverWorker(features: NodeResolutionFeatures, modu
// es module file or cjs-like input file, use a variant of the legacy cjs resolver that supports the selected modern features
const esmMode = resolutionMode === ModuleKind.ESNext ? NodeResolutionFeatures.EsmMode : 0;
let extensions = compilerOptions.noDtsResolution ? Extensions.ImplementationFiles : Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration;
if (compilerOptions.resolveJsonModule) {
if (getResolveJsonModule(compilerOptions)) {
extensions |= Extensions.Json;
}
return nodeModuleNameResolverWorker(features | esmMode, moduleName, containingDirectory, compilerOptions, host, cache, extensions, /*isConfigLookup*/ false, redirectedReference);
@@ -1547,6 +1601,15 @@ function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host:
/*redirectedReferences*/ undefined);
}
export function bundlerModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations {
const containingDirectory = getDirectoryPath(containingFile);
let extensions = compilerOptions.noDtsResolution ? Extensions.ImplementationFiles : Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration;
if (getResolveJsonModule(compilerOptions)) {
extensions |= Extensions.Json;
}
return nodeModuleNameResolverWorker(getNodeResolutionFeatures(compilerOptions), moduleName, containingDirectory, compilerOptions, host, cache, extensions, /*isConfigLookup*/ false, redirectedReference);
}
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations;
/** @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, isConfigLookup?: boolean): ResolvedModuleWithFailedLookupLocations {
@@ -1556,10 +1619,10 @@ export function nodeModuleNameResolver(moduleName: string, containingFile: strin
}
else if (compilerOptions.noDtsResolution) {
extensions = Extensions.ImplementationFiles;
if (compilerOptions.resolveJsonModule) extensions |= Extensions.Json;
if (getResolveJsonModule(compilerOptions)) extensions |= Extensions.Json;
}
else {
extensions = compilerOptions.resolveJsonModule
extensions = getResolveJsonModule(compilerOptions)
? Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration | Extensions.Json
: Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration;
}
@@ -1571,12 +1634,7 @@ function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleNa
const failedLookupLocations: string[] = [];
const affectingLocations: string[] = [];
// conditions are only used by the node16/nodenext resolver - there's no priority order in the list,
//it's essentially a set (priority is determined by object insertion order in the object we look at).
const conditions = features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"];
if (compilerOptions.noDtsResolution) {
conditions.pop();
}
const conditions = getConditions(compilerOptions, !!(features & NodeResolutionFeatures.EsmMode));
const diagnostics: Diagnostic[] = [];
const state: ModuleResolutionState = {
@@ -1720,7 +1778,7 @@ function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string,
}
}
// esm mode relative imports shouldn't do any directory lookups (either inside `package.json`
// files or implicit `index.js`es). This is a notable depature from cjs norms, where `./foo/pkg`
// files or implicit `index.js`es). This is a notable departure from cjs norms, where `./foo/pkg`
// could have been redirected by `./foo/pkg/package.json` to an arbitrary location!
if (!(state.features & NodeResolutionFeatures.EsmMode)) {
return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson);
@@ -1795,7 +1853,12 @@ 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)) {
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) {
@@ -1810,7 +1873,7 @@ function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, only
extensions & Extensions.Declaration && fileExtensionIsOneOf(candidate, supportedDeclarationExtensions)
) {
const result = tryFile(candidate, onlyRecordFailures, state);
return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension } : undefined;
return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension, resolvedUsingTsExtension: undefined } : undefined;
}
return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state);
@@ -1854,6 +1917,13 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
if (result) return result;
}
return undefined;
case Extension.Ts:
case Extension.Tsx:
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)
@@ -1863,9 +1933,9 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
}
function tryExtension(ext: Extension): PathAndExtension | undefined {
function tryExtension(ext: Extension, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
const path = tryFile(candidate + ext, onlyRecordFailures, state);
return path === undefined ? undefined : { path, ext };
return path === undefined ? undefined : { path, ext, resolvedUsingTsExtension };
}
}
@@ -1921,26 +1991,30 @@ export function getEntrypointsFromPackageJsonInfo(
let entrypoints: string[] | undefined;
const extensions = Extensions.TypeScript | Extensions.Declaration | (resolveJs ? Extensions.JavaScript : 0);
const features = getDefaultNodeResolutionFeatures(options);
const requireState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
requireState.conditions = ["node", "require", "types"];
requireState.requestContainingDirectory = packageJsonInfo.packageDirectory;
const requireResolution = loadNodeModuleFromDirectoryWorker(
const features = getNodeResolutionFeatures(options);
const loadPackageJsonMainState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
loadPackageJsonMainState.conditions = getConditions(options);
loadPackageJsonMainState.requestContainingDirectory = packageJsonInfo.packageDirectory;
const mainResolution = loadNodeModuleFromDirectoryWorker(
extensions,
packageJsonInfo.packageDirectory,
/*onlyRecordFailures*/ false,
requireState,
loadPackageJsonMainState,
packageJsonInfo.contents.packageJsonContent,
getVersionPathsOfPackageJsonInfo(packageJsonInfo, requireState));
entrypoints = append(entrypoints, requireResolution?.path);
getVersionPathsOfPackageJsonInfo(packageJsonInfo, loadPackageJsonMainState));
entrypoints = append(entrypoints, mainResolution?.path);
if (features & NodeResolutionFeatures.Exports && packageJsonInfo.contents.packageJsonContent.exports) {
for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) {
const exportState = { ...requireState, failedLookupLocations: [], conditions };
const conditionSets = deduplicate(
[getConditions(options, /*esmMode*/ true), getConditions(options, /*esmMode*/ false)],
arrayIsEqualTo
);
for (const conditions of conditionSets) {
const loadPackageJsonExportsState = { ...loadPackageJsonMainState, failedLookupLocations: [], conditions };
const exportResolutions = loadEntrypointsFromExportMap(
packageJsonInfo,
packageJsonInfo.contents.packageJsonContent.exports,
exportState,
loadPackageJsonExportsState,
extensions);
if (exportResolutions) {
for (const resolution of exportResolutions) {
@@ -2046,7 +2120,7 @@ export interface PackageJsonInfoContents {
*
* @internal
*/
export function getPackageScopeForPath(fileName: string, state: ModuleResolutionState): PackageJsonInfo | undefined {
export function getPackageScopeForPath(fileName: string, state: ModuleResolutionState): PackageJsonInfo | undefined {
const parts = getPathComponents(fileName);
parts.pop();
while (parts.length > 0) {
@@ -2179,9 +2253,9 @@ function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: st
}
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined {
function resolvedIfExtensionMatches(extensions: Extensions, path: string, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
const ext = tryGetExtensionFromPath(path);
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined;
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext, resolvedUsingTsExtension } : undefined;
}
/** True if `extension` is one of the supported `extensions`. */
@@ -2372,7 +2446,13 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
traceIfEnabled(state, Diagnostics.Using_0_subpath_1_with_target_2, "imports", key, combinedLookup);
traceIfEnabled(state, Diagnostics.Resolving_module_0_from_1, combinedLookup, scope.packageDirectory + "/");
const result = nodeModuleNameResolverWorker(state.features, combinedLookup, scope.packageDirectory + "/", state.compilerOptions, state.host, cache, extensions, /*isConfigLookup*/ false, redirectedReference);
return toSearchResult(result.resolvedModule ? { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId, originalPath: result.resolvedModule.originalPath } : undefined);
return toSearchResult(result.resolvedModule ? {
path: result.resolvedModule.resolvedFileName,
extension: result.resolvedModule.extension,
packageId: result.resolvedModule.packageId,
originalPath: result.resolvedModule.originalPath,
resolvedUsingTsExtension: result.resolvedModule.resolvedUsingTsExtension
} : undefined);
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName);
@@ -2753,7 +2833,7 @@ function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, bas
if (extension !== undefined) {
const path = tryFile(candidate, onlyRecordFailures, state);
if (path !== undefined) {
return noPackageId({ path, ext: extension });
return noPackageId({ path, ext: extension, resolvedUsingTsExtension: undefined });
}
}
return loader(extensions, candidate, onlyRecordFailures || !directoryProbablyExists(getDirectoryPath(candidate), state.host), state);
@@ -2813,7 +2893,15 @@ function tryFindNonRelativeModuleNameInCache(cache: NonRelativeModuleNameResolut
trace(state.host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory);
}
state.resultFromCache = result;
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, originalPath: result.resolvedModule.originalPath || true, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } };
return {
value: result.resolvedModule && {
path: result.resolvedModule.resolvedFileName,
originalPath: result.resolvedModule.originalPath || true,
extension: result.resolvedModule.extension,
packageId: result.resolvedModule.packageId,
resolvedUsingTsExtension: result.resolvedModule.resolvedUsingTsExtension
}
};
}
}
@@ -2880,6 +2968,18 @@ export function classicNameResolver(moduleName: string, containingFile: string,
}
}
export function moduleResolutionSupportsResolvingTsExtensions(compilerOptions: CompilerOptions) {
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler;
}
// Program errors validate that `noEmit` or `emitDeclarationOnly` is also set,
// so this function doesn't check them to avoid propagating errors.
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string) {
return moduleResolutionSupportsResolvingTsExtensions(compilerOptions) && (
!!compilerOptions.allowImportingTsExtensions ||
fromFileName && isDeclarationFileName(fromFileName));
}
/**
* A host may load a module from a global cache of typings.
* This is the minumum code needed to expose that functionality; the rest is in the host.