Skip to content

Commit 67172e4

Browse files
weswighamsandersn
andauthored
Triple-slash reference type directives can override the import mode used for their resolution (#47732)
* Triple-slash reference type directives can override the import mode used for their resolution They now use the file's default mode by default, rather than always using commonjs. The new arguments to the reference directive look like: ```ts ///<reference types="pkg" resolution-mode="require" /> ``` or ```ts ///<reference types="pkg" resolution-mode="import" /> ``` * Omit redundant import modes in emitter * Add test for #47806 * Add server test for triple-slash reference mode overrides * Move FileReference mode into helper * Update tests/cases/conformance/node/nodeModulesTripleSlashReferenceModeOverride3.ts Co-authored-by: Nathan Shively-Sanders <[email protected]> Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent 1b1530a commit 67172e4

File tree

129 files changed

+3158
-102
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+3158
-102
lines changed

src/compiler/checker.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -42202,19 +42202,19 @@ namespace ts {
4220242202
// this variable and functions that use it are deliberately moved here from the outer scope
4220342203
// to avoid scope pollution
4220442204
const resolvedTypeReferenceDirectives = host.getResolvedTypeReferenceDirectives();
42205-
let fileToDirective: ESMap<string, string>;
42205+
let fileToDirective: ESMap<string, [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined]>;
4220642206
if (resolvedTypeReferenceDirectives) {
4220742207
// populate reverse mapping: file path -> type reference directive that was resolved to this file
42208-
fileToDirective = new Map<string, string>();
42209-
resolvedTypeReferenceDirectives.forEach((resolvedDirective, key) => {
42208+
fileToDirective = new Map<string, [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined]>();
42209+
resolvedTypeReferenceDirectives.forEach((resolvedDirective, key, mode) => {
4221042210
if (!resolvedDirective || !resolvedDirective.resolvedFileName) {
4221142211
return;
4221242212
}
4221342213
const file = host.getSourceFile(resolvedDirective.resolvedFileName);
4221442214
if (file) {
4221542215
// Add the transitive closure of path references loaded by this file (as long as they are not)
4221642216
// part of an existing type reference.
42217-
addReferencedFilesToTypeDirective(file, key);
42217+
addReferencedFilesToTypeDirective(file, key, mode);
4221842218
}
4221942219
});
4222042220
}
@@ -42337,7 +42337,7 @@ namespace ts {
4233742337
}
4233842338

4233942339
// defined here to avoid outer scope pollution
42340-
function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): string[] | undefined {
42340+
function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined {
4234142341
// program does not have any files with type reference directives - bail out
4234242342
if (!fileToDirective) {
4234342343
return undefined;
@@ -42355,13 +42355,13 @@ namespace ts {
4235542355
}
4235642356

4235742357
// defined here to avoid outer scope pollution
42358-
function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[] | undefined {
42358+
function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined {
4235942359
// program does not have any files with type reference directives - bail out
4236042360
if (!fileToDirective || !isSymbolFromTypeDeclarationFile(symbol)) {
4236142361
return undefined;
4236242362
}
4236342363
// check what declarations in the symbol can contribute to the target meaning
42364-
let typeReferenceDirectives: string[] | undefined;
42364+
let typeReferenceDirectives: [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined;
4236542365
for (const decl of symbol.declarations!) {
4236642366
// check meaning of the local symbol to see if declaration needs to be analyzed further
4236742367
if (decl.symbol && decl.symbol.flags & meaning!) {
@@ -42412,14 +42412,14 @@ namespace ts {
4241242412
return false;
4241342413
}
4241442414

42415-
function addReferencedFilesToTypeDirective(file: SourceFile, key: string) {
42415+
function addReferencedFilesToTypeDirective(file: SourceFile, key: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
4241642416
if (fileToDirective.has(file.path)) return;
42417-
fileToDirective.set(file.path, key);
42418-
for (const { fileName } of file.referencedFiles) {
42417+
fileToDirective.set(file.path, [key, mode]);
42418+
for (const { fileName, resolutionMode } of file.referencedFiles) {
4241942419
const resolvedFile = resolveTripleslashReference(fileName, file.fileName);
4242042420
const referencedFile = host.getSourceFile(resolvedFile);
4242142421
if (referencedFile) {
42422-
addReferencedFilesToTypeDirective(referencedFile, key);
42422+
addReferencedFilesToTypeDirective(referencedFile, key, resolutionMode || file.impliedNodeFormat);
4242342423
}
4242442424
}
4242542425
}

src/compiler/diagnosticMessages.json

+8
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,14 @@
14121412
"category": "Error",
14131413
"code": 1451
14141414
},
1415+
"Resolution modes are only supported when `moduleResolution` is `node12` or `nodenext`.": {
1416+
"category": "Error",
1417+
"code": 1452
1418+
},
1419+
"`resolution-mode` should be either `require` or `import`.": {
1420+
"category": "Error",
1421+
"code": 1453
1422+
},
14151423

14161424
"The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.": {
14171425
"category": "Error",

src/compiler/emitter.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -3993,8 +3993,11 @@ namespace ts {
39933993
}
39943994
for (const directive of types) {
39953995
const pos = writer.getTextPos();
3996-
writeComment(`/// <reference types="${directive.fileName}" />`);
3997-
if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: BundleFileSectionKind.Type, data: directive.fileName });
3996+
const resolutionMode = directive.resolutionMode && directive.resolutionMode !== currentSourceFile?.impliedNodeFormat
3997+
? `resolution-mode="${directive.resolutionMode === ModuleKind.ESNext ? "import" : "require"}"`
3998+
: "";
3999+
writeComment(`/// <reference types="${directive.fileName}" ${resolutionMode}/>`);
4000+
if (bundleFileInfo) bundleFileInfo.sections.push({ pos, end: writer.getTextPos(), kind: !directive.resolutionMode ? BundleFileSectionKind.Type : directive.resolutionMode === ModuleKind.ESNext ? BundleFileSectionKind.TypeResolutionModeImport : BundleFileSectionKind.TypeResolutionModeRequire, data: directive.fileName });
39984001
writeLine();
39994002
}
40004003
for (const directive of libs) {

src/compiler/factory/nodeFactory.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -6414,7 +6414,7 @@ namespace ts {
64146414
let prologues: UnparsedPrologue[] | undefined;
64156415
let helpers: UnscopedEmitHelper[] | undefined;
64166416
let referencedFiles: FileReference[] | undefined;
6417-
let typeReferenceDirectives: string[] | undefined;
6417+
let typeReferenceDirectives: FileReference[] | undefined;
64186418
let libReferenceDirectives: FileReference[] | undefined;
64196419
let prependChildren: UnparsedTextLike[] | undefined;
64206420
let texts: UnparsedSourceText[] | undefined;
@@ -6435,7 +6435,13 @@ namespace ts {
64356435
referencedFiles = append(referencedFiles, { pos: -1, end: -1, fileName: section.data });
64366436
break;
64376437
case BundleFileSectionKind.Type:
6438-
typeReferenceDirectives = append(typeReferenceDirectives, section.data);
6438+
typeReferenceDirectives = append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data });
6439+
break;
6440+
case BundleFileSectionKind.TypeResolutionModeImport:
6441+
typeReferenceDirectives = append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ModuleKind.ESNext });
6442+
break;
6443+
case BundleFileSectionKind.TypeResolutionModeRequire:
6444+
typeReferenceDirectives = append(typeReferenceDirectives, { pos: -1, end: -1, fileName: section.data, resolutionMode: ModuleKind.CommonJS });
64396445
break;
64406446
case BundleFileSectionKind.Lib:
64416447
libReferenceDirectives = append(libReferenceDirectives, { pos: -1, end: -1, fileName: section.data });
@@ -6496,6 +6502,8 @@ namespace ts {
64966502
case BundleFileSectionKind.NoDefaultLib:
64976503
case BundleFileSectionKind.Reference:
64986504
case BundleFileSectionKind.Type:
6505+
case BundleFileSectionKind.TypeResolutionModeImport:
6506+
case BundleFileSectionKind.TypeResolutionModeRequire:
64996507
case BundleFileSectionKind.Lib:
65006508
syntheticReferences = append(syntheticReferences, setTextRange(factory.createUnparsedSyntheticReference(section), section));
65016509
break;

src/compiler/moduleNameResolver.ts

+22-7
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,15 @@ namespace ts {
297297
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
298298
* is assumed to be the same as root directory of the project.
299299
*/
300-
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
300+
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, cache?: TypeReferenceDirectiveResolutionCache, resolutionMode?: SourceFile["impliedNodeFormat"]): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
301301
const traceEnabled = isTraceEnabled(options, host);
302302
if (redirectedReference) {
303303
options = redirectedReference.commandLine.options;
304304
}
305305

306306
const containingDirectory = containingFile ? getDirectoryPath(containingFile) : undefined;
307307
const perFolderCache = containingDirectory ? cache && cache.getOrCreateCacheForDirectory(containingDirectory, redirectedReference) : undefined;
308-
let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName, /*mode*/ undefined);
308+
let result = perFolderCache && perFolderCache.get(typeReferenceDirectiveName, /*mode*/ resolutionMode);
309309
if (result) {
310310
if (traceEnabled) {
311311
trace(host, Diagnostics.Resolving_type_reference_directive_0_containing_file_1, typeReferenceDirectiveName, containingFile);
@@ -340,8 +340,19 @@ namespace ts {
340340
}
341341

342342
const failedLookupLocations: string[] = [];
343-
const features = getDefaultNodeResolutionFeatures(options);
344-
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions: ["node", "require", "types"] };
343+
let features = getDefaultNodeResolutionFeatures(options);
344+
// Unlike `import` statements, whose mode-calculating APIs are all guaranteed to return `undefined` if we're in an un-mode-ed module resolution
345+
// setting, type references will return their target mode regardless of options because of how the parser works, so we guard against the mode being
346+
// set in a non-modal module resolution setting here. Do note that our behavior is not particularly well defined when these mode-overriding imports
347+
// are present in a non-modal project; while in theory we'd like to either ignore the mode or provide faithful modern resolution, depending on what we feel is best,
348+
// in practice, not every cache has the options available to intelligently make the choice to ignore the mode request, and it's unclear how modern "faithful modern
349+
// resolution" should be (`node12`? `nodenext`?). As such, witnessing a mode-overriding triple-slash reference in a non-modal module resolution
350+
// context should _probably_ be an error - and that should likely be handled by the `Program` (which is what we do).
351+
if (resolutionMode === ModuleKind.ESNext && (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext)) {
352+
features |= NodeResolutionFeatures.EsmMode;
353+
}
354+
const conditions = features & NodeResolutionFeatures.Exports ? features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] : [];
355+
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache, features, conditions };
345356
let resolved = primaryLookup();
346357
let primary = true;
347358
if (!resolved) {
@@ -362,7 +373,7 @@ namespace ts {
362373
};
363374
}
364375
result = { resolvedTypeReferenceDirective, failedLookupLocations };
365-
perFolderCache?.set(typeReferenceDirectiveName, /*mode*/ undefined, result);
376+
perFolderCache?.set(typeReferenceDirectiveName, /*mode*/ resolutionMode, result);
366377
if (traceEnabled) traceResult(result);
367378
return result;
368379

@@ -733,11 +744,15 @@ namespace ts {
733744
}
734745

735746
/* @internal */
736-
export function zipToModeAwareCache<V>(file: SourceFile, keys: readonly string[], values: readonly V[]): ModeAwareCache<V> {
747+
export function zipToModeAwareCache<V>(file: SourceFile, keys: readonly string[] | readonly FileReference[], values: readonly V[]): ModeAwareCache<V> {
737748
Debug.assert(keys.length === values.length);
738749
const map = createModeAwareCache<V>();
739750
for (let i = 0; i < keys.length; ++i) {
740-
map.set(keys[i], getModeForResolutionAtIndex(file, i), values[i]);
751+
const entry = keys[i];
752+
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.
753+
const name = !isString(entry) ? entry.fileName.toLowerCase() : entry;
754+
const mode = !isString(entry) ? entry.resolutionMode || file.impliedNodeFormat : getModeForResolutionAtIndex(file, i);
755+
map.set(name, mode, values[i]);
741756
}
742757
return map;
743758
}

src/compiler/parser.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -9306,6 +9306,20 @@ namespace ts {
93069306
moduleName?: string;
93079307
}
93089308

9309+
function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ModuleKind.ESNext | ModuleKind.CommonJS | undefined {
9310+
if (!mode) {
9311+
return undefined;
9312+
}
9313+
if (mode === "import") {
9314+
return ModuleKind.ESNext;
9315+
}
9316+
if (mode === "require") {
9317+
return ModuleKind.CommonJS;
9318+
}
9319+
reportDiagnostic(pos, end - pos, Diagnostics.resolution_mode_should_be_either_require_or_import);
9320+
return undefined;
9321+
}
9322+
93099323
/*@internal*/
93109324
export function processCommentPragmas(context: PragmaContext, sourceText: string): void {
93119325
const pragmas: PragmaPseudoMapEntry[] = [];
@@ -9351,12 +9365,13 @@ namespace ts {
93519365
const typeReferenceDirectives = context.typeReferenceDirectives;
93529366
const libReferenceDirectives = context.libReferenceDirectives;
93539367
forEach(toArray(entryOrList) as PragmaPseudoMap["reference"][], arg => {
9354-
const { types, lib, path } = arg.arguments;
9368+
const { types, lib, path, ["resolution-mode"]: res } = arg.arguments;
93559369
if (arg.arguments["no-default-lib"]) {
93569370
context.hasNoDefaultLib = true;
93579371
}
93589372
else if (types) {
9359-
typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value });
9373+
const parsed = parseResolutionMode(res, types.pos, types.end, reportDiagnostic);
9374+
typeReferenceDirectives.push({ pos: types.pos, end: types.end, fileName: types.value, ...(parsed ? { resolutionMode: parsed } : {}) });
93609375
}
93619376
else if (lib) {
93629377
libReferenceDirectives.push({ pos: lib.pos, end: lib.end, fileName: lib.value });

0 commit comments

Comments
 (0)