Skip to content

Commit 9ada046

Browse files
committed
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" /> ```
1 parent b7b6483 commit 9ada046

File tree

127 files changed

+3086
-100
lines changed

Some content is hidden

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

127 files changed

+3086
-100
lines changed

src/compiler/checker.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42120,19 +42120,19 @@ namespace ts {
4212042120
// this variable and functions that use it are deliberately moved here from the outer scope
4212142121
// to avoid scope pollution
4212242122
const resolvedTypeReferenceDirectives = host.getResolvedTypeReferenceDirectives();
42123-
let fileToDirective: ESMap<string, string>;
42123+
let fileToDirective: ESMap<string, [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined]>;
4212442124
if (resolvedTypeReferenceDirectives) {
4212542125
// populate reverse mapping: file path -> type reference directive that was resolved to this file
42126-
fileToDirective = new Map<string, string>();
42127-
resolvedTypeReferenceDirectives.forEach((resolvedDirective, key) => {
42126+
fileToDirective = new Map<string, [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined]>();
42127+
resolvedTypeReferenceDirectives.forEach((resolvedDirective, key, mode) => {
4212842128
if (!resolvedDirective || !resolvedDirective.resolvedFileName) {
4212942129
return;
4213042130
}
4213142131
const file = host.getSourceFile(resolvedDirective.resolvedFileName);
4213242132
if (file) {
4213342133
// Add the transitive closure of path references loaded by this file (as long as they are not)
4213442134
// part of an existing type reference.
42135-
addReferencedFilesToTypeDirective(file, key);
42135+
addReferencedFilesToTypeDirective(file, key, mode);
4213642136
}
4213742137
});
4213842138
}
@@ -42255,7 +42255,7 @@ namespace ts {
4225542255
}
4225642256

4225742257
// defined here to avoid outer scope pollution
42258-
function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): string[] | undefined {
42258+
function getTypeReferenceDirectivesForEntityName(node: EntityNameOrEntityNameExpression): [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined {
4225942259
// program does not have any files with type reference directives - bail out
4226042260
if (!fileToDirective) {
4226142261
return undefined;
@@ -42273,13 +42273,13 @@ namespace ts {
4227342273
}
4227442274

4227542275
// defined here to avoid outer scope pollution
42276-
function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): string[] | undefined {
42276+
function getTypeReferenceDirectivesForSymbol(symbol: Symbol, meaning?: SymbolFlags): [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined {
4227742277
// program does not have any files with type reference directives - bail out
4227842278
if (!fileToDirective || !isSymbolFromTypeDeclarationFile(symbol)) {
4227942279
return undefined;
4228042280
}
4228142281
// check what declarations in the symbol can contribute to the target meaning
42282-
let typeReferenceDirectives: string[] | undefined;
42282+
let typeReferenceDirectives: [specifier: string, mode: SourceFile["impliedNodeFormat"] | undefined][] | undefined;
4228342283
for (const decl of symbol.declarations!) {
4228442284
// check meaning of the local symbol to see if declaration needs to be analyzed further
4228542285
if (decl.symbol && decl.symbol.flags & meaning!) {
@@ -42330,14 +42330,14 @@ namespace ts {
4233042330
return false;
4233142331
}
4233242332

42333-
function addReferencedFilesToTypeDirective(file: SourceFile, key: string) {
42333+
function addReferencedFilesToTypeDirective(file: SourceFile, key: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
4233442334
if (fileToDirective.has(file.path)) return;
42335-
fileToDirective.set(file.path, key);
42336-
for (const { fileName } of file.referencedFiles) {
42335+
fileToDirective.set(file.path, [key, mode]);
42336+
for (const { fileName, resolutionMode } of file.referencedFiles) {
4233742337
const resolvedFile = resolveTripleslashReference(fileName, file.fileName);
4233842338
const referencedFile = host.getSourceFile(resolvedFile);
4233942339
if (referencedFile) {
42340-
addReferencedFilesToTypeDirective(referencedFile, key);
42340+
addReferencedFilesToTypeDirective(referencedFile, key, resolutionMode || file.impliedNodeFormat);
4234142341
}
4234242342
}
4234342343
}

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,14 @@
14001400
"category": "Error",
14011401
"code": 1451
14021402
},
1403+
"Resolution modes are only supported when `moduleResolution` is `node12` or `nodenext`.": {
1404+
"category": "Error",
1405+
"code": 1452
1406+
},
1407+
"`resolution-mode` should be either `require` or `import`.": {
1408+
"category": "Error",
1409+
"code": 1453
1410+
},
14031411

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

src/compiler/emitter.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3993,8 +3993,9 @@ 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+
// Should we elide `resolution-mode` if it matches the mode the currentSourceFile defaults to?
3997+
writeComment(`/// <reference types="${directive.fileName}" ${directive.resolutionMode ? `resolution-mode="${directive.resolutionMode === ModuleKind.ESNext ? "import" : "require"}"` : ""}/>`);
3998+
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 });
39983999
writeLine();
39994000
}
40004001
for (const directive of libs) {

src/compiler/factory/nodeFactory.ts

Lines changed: 10 additions & 2 deletions
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

Lines changed: 22 additions & 7 deletions
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`.
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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9305,6 +9305,20 @@ namespace ts {
93059305
moduleName?: string;
93069306
}
93079307

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

0 commit comments

Comments
 (0)