Skip to content

Commit 015babb

Browse files
committed
Initial support for 'typesVersions'
1 parent 926bdee commit 015babb

36 files changed

+687
-53
lines changed

src/compiler/checker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2255,7 +2255,7 @@ namespace ts {
22552255
? Diagnostics.If_the_0_package_actually_exposes_this_module_consider_sending_a_pull_request_to_amend_https_Colon_Slash_Slashgithub_com_SlashDefinitelyTyped_SlashDefinitelyTyped_Slashtree_Slashmaster_Slashtypes_Slash_1
22562256
: Diagnostics.Try_npm_install_types_Slash_1_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0,
22572257
packageId.name,
2258-
getMangledNameForScopedPackage(packageId.name))
2258+
mangleScopedPackageName(packageId.name))
22592259
: undefined;
22602260
errorOrSuggestion(isError, errorNode, chainDiagnosticMessages(
22612261
errorInfo,

src/compiler/diagnosticMessages.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3273,7 +3273,7 @@
32733273
"category": "Message",
32743274
"code": 6104
32753275
},
3276-
"Expected type of '{0}' field in 'package.json' to be 'string', got '{1}'.": {
3276+
"Expected type of '{0}' field in 'package.json' to be '{1}', got '{2}'.": {
32773277
"category": "Message",
32783278
"code": 6105
32793279
},

src/compiler/moduleNameResolver.ts

Lines changed: 105 additions & 47 deletions
Large diffs are not rendered by default.

src/compiler/moduleSpecifiers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ namespace ts.moduleSpecifiers {
326326
// if node_modules folder is in this folder or any of its parent folders, no need to keep it.
327327
if (!startsWith(sourceDirectory, getCanonicalFileName(moduleSpecifier.substring(0, parts.topLevelNodeModulesIndex)))) return undefined;
328328
// If the module was found in @types, get the actual Node package name
329-
return getPackageNameFromAtTypesDirectory(moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1));
329+
return getPackageNameFromTypesPackageName(moduleSpecifier.substring(parts.topLevelPackageNameIndex + 1));
330330

331331
function getDirectoryOrExtensionlessFileName(path: string): string {
332332
// If the file is the main module, it can be imported by the package name

src/compiler/semver.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/* @internal */
2+
namespace ts {
3+
// Per https://semver.org/#spec-item-2:
4+
//
5+
// > A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative
6+
// > integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor
7+
// > version, and Z is the patch version. Each element MUST increase numerically.
8+
//
9+
// NOTE: We differ here in that we allow X and X.Y, with missing parts having the default
10+
// value of `0`.
11+
const versionRegExp = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:-([a-z0-9-.]+))?(?:(\+[a-z0-9-.]+))?)?)?$/i;
12+
13+
// Per https://semver.org/#spec-item-9:
14+
//
15+
// > A pre-release version MAY be denoted by appending a hyphen and a series of dot separated
16+
// > identifiers immediately following the patch version. Identifiers MUST comprise only ASCII
17+
// > alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers
18+
// > MUST NOT include leading zeroes.
19+
const prereleaseRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)(?:\.(?:0|[1-9]\d*|[a-z-][a-z0-9-]*))*$/i;
20+
21+
// Per https://semver.org/#spec-item-10:
22+
//
23+
// > Build metadata MAY be denoted by appending a plus sign and a series of dot separated
24+
// > identifiers immediately following the patch or pre-release version. Identifiers MUST
25+
// > comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
26+
const buildRegExp = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i;
27+
28+
// Per https://semver.org/#spec-item-9:
29+
//
30+
// > Numeric identifiers MUST NOT include leading zeroes.
31+
const numericIdentifierRegExp = /^(0|[1-9]\d*)$/;
32+
33+
/**
34+
* Describes a precise semantic version number, per https://semver.org
35+
*/
36+
export class Version {
37+
static readonly zero = new Version(0);
38+
39+
readonly major: number;
40+
readonly minor: number;
41+
readonly patch: number;
42+
readonly prerelease: ReadonlyArray<string>;
43+
readonly build: ReadonlyArray<string>;
44+
45+
constructor(major: number, minor = 0, patch = 0, prerelease = "", build = "") {
46+
Debug.assert(major >= 0, "Invalid argument: major");
47+
Debug.assert(minor >= 0, "Invalid argument: minor");
48+
Debug.assert(patch >= 0, "Invalid argument: patch");
49+
Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease");
50+
Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build");
51+
this.major = major;
52+
this.minor = minor;
53+
this.patch = patch;
54+
this.prerelease = prerelease === "" ? emptyArray : prerelease.split(".");
55+
this.build = build === "" ? emptyArray : build.split(".");
56+
}
57+
58+
static parse(text: string) {
59+
return Debug.assertDefined(this.tryParse(text));
60+
}
61+
62+
static tryParse(text: string) {
63+
const match = versionRegExp.exec(text);
64+
if (!match) return undefined;
65+
66+
const [, major, minor = 0, patch = 0, prerelease, build] = match;
67+
if (prerelease && !prereleaseRegExp.test(prerelease)) return undefined;
68+
if (build && !buildRegExp.test(build)) return undefined;
69+
return new Version(+major, +minor, +patch, prerelease, build);
70+
}
71+
72+
static compare(left: Version | undefined, right: Version | undefined, compareBuildMetadata?: boolean) {
73+
// Per https://semver.org/#spec-item-11:
74+
//
75+
// > Precedence is determined by the first difference when comparing each of these
76+
// > identifiers from left to right as follows: Major, minor, and patch versions are
77+
// > always compared numerically.
78+
//
79+
// > When major, minor, and patch are equal, a pre-release version has lower
80+
// > precedence than a normal version.
81+
//
82+
// Per https://semver.org/#spec-item-10:
83+
//
84+
// > Build metadata SHOULD be ignored when determining version precedence.
85+
if (left === right) return Comparison.EqualTo;
86+
if (left === undefined) return Comparison.LessThan;
87+
if (right === undefined) return Comparison.GreaterThan;
88+
return compareValues(left.major, right.major)
89+
|| compareValues(left.minor, right.minor)
90+
|| compareValues(left.patch, right.patch)
91+
|| compareVersionFragments(left.prerelease, right.prerelease, /*compareNumericIdentifiers*/ true)
92+
|| (compareBuildMetadata ? compareVersionFragments(left.build, right.build, /*compareNumericIdentifiers*/ false) : Comparison.EqualTo);
93+
}
94+
95+
compareTo(other: Version, compareBuildMetadata?: boolean) {
96+
return Version.compare(this, other, compareBuildMetadata);
97+
}
98+
99+
toString() {
100+
let result = `${this.major}.${this.minor}.${this.patch}`;
101+
if (this.prerelease) result += `-${this.prerelease.join(".")}`;
102+
if (this.build) result += `+${this.build.join(".")}`;
103+
return result;
104+
}
105+
}
106+
107+
function compareVersionFragments(left: ReadonlyArray<string>, right: ReadonlyArray<string>, compareNumericIdentifiers: boolean) {
108+
// Per https://semver.org/#spec-item-11:
109+
//
110+
// > When major, minor, and patch are equal, a pre-release version has lower precedence
111+
// > than a normal version.
112+
if (left === right) return Comparison.EqualTo;
113+
if (left.length === 0) return right.length === 0 ? Comparison.EqualTo : Comparison.GreaterThan;
114+
if (right.length === 0) return Comparison.LessThan;
115+
116+
// Per https://semver.org/#spec-item-11:
117+
//
118+
// > Precedence for two pre-release versions with the same major, minor, and patch version
119+
// > MUST be determined by comparing each dot separated identifier from left to right until
120+
// > a difference is found
121+
const length = Math.min(left.length, right.length);
122+
for (let i = 0; i < length; i++) {
123+
const leftIdentifier = left[i];
124+
const rightIdentifier = right[i];
125+
if (leftIdentifier === rightIdentifier) continue;
126+
127+
const leftIsNumeric = compareNumericIdentifiers && numericIdentifierRegExp.test(leftIdentifier);
128+
const rightIsNumeric = compareNumericIdentifiers && numericIdentifierRegExp.test(rightIdentifier);
129+
if (leftIsNumeric || rightIsNumeric) {
130+
// Per https://semver.org/#spec-item-11:
131+
//
132+
// > Numeric identifiers always have lower precedence than non-numeric identifiers.
133+
if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? Comparison.LessThan : Comparison.GreaterThan;
134+
135+
// Per https://semver.org/#spec-item-11:
136+
//
137+
// > identifiers consisting of only digits are compared numerically
138+
const result = compareValues(+leftIdentifier, +rightIdentifier);
139+
if (result) return result;
140+
}
141+
else {
142+
// Per https://semver.org/#spec-item-11:
143+
//
144+
// > identifiers with letters or hyphens are compared lexically in ASCII sort order.
145+
const result = compareStringsCaseSensitive(leftIdentifier, rightIdentifier);
146+
if (result) return result;
147+
}
148+
}
149+
150+
// Per https://semver.org/#spec-item-11:
151+
//
152+
// > A larger set of pre-release fields has a higher precedence than a smaller set, if all
153+
// > of the preceding identifiers are equal.
154+
return compareValues(left.length, right.length);
155+
}
156+
}

src/compiler/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"files": [
1010
"core.ts",
1111
"performance.ts",
12+
"semver.ts",
1213

1314
"types.ts",
1415
"sys.ts",

src/compiler/utilities.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3940,6 +3940,27 @@ namespace ts {
39403940
return getStringFromExpandedCharCodes(expandedCharCodes);
39413941
}
39423942

3943+
export function readJson(path: string, host: { readFile(fileName: string): string | undefined }): object {
3944+
try {
3945+
const jsonText = host.readFile(path);
3946+
if (!jsonText) return {};
3947+
const result = parseConfigFileTextToJson(path, jsonText);
3948+
if (result.error) {
3949+
return {};
3950+
}
3951+
return result.config;
3952+
}
3953+
catch (e) {
3954+
// gracefully handle if readFile fails or returns not JSON
3955+
return {};
3956+
}
3957+
}
3958+
3959+
export function directoryProbablyExists(directoryName: string, host: { directoryExists?: (directoryName: string) => boolean }): boolean {
3960+
// if host does not support 'directoryExists' assume that directory will exist
3961+
return !host.directoryExists || host.directoryExists(directoryName);
3962+
}
3963+
39433964
const carriageReturnLineFeed = "\r\n";
39443965
const lineFeed = "\n";
39453966
export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, getNewLine?: () => string): string {

src/services/codefixes/fixCannotFindModule.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ namespace ts.codefix {
2929

3030
function getTypesPackageNameToInstall(host: LanguageServiceHost, sourceFile: SourceFile, pos: number, diagCode: number): string | undefined {
3131
const moduleName = cast(getTokenAtPosition(sourceFile, pos), isStringLiteral).text;
32-
const { packageName } = getPackageName(moduleName);
32+
const { packageName } = parsePackageName(moduleName);
3333
return diagCode === errorCodeCannotFindModule
3434
? (JsTyping.nodeCoreModules.has(packageName) ? "@types/node" : undefined)
3535
: (host.isKnownTypesPackageName!(packageName) ? getTypesPackageName(packageName) : undefined); // TODO: GH#18217

src/services/pathCompletions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ namespace ts.Completions.PathCompletions {
329329
const seen = createMap<true>();
330330
if (options.types) {
331331
for (const typesName of options.types) {
332-
const moduleName = getUnmangledNameForScopedPackage(typesName);
332+
const moduleName = unmangleScopedPackageName(typesName);
333333
pushResult(moduleName);
334334
}
335335
}
@@ -363,7 +363,7 @@ namespace ts.Completions.PathCompletions {
363363
for (let typeDirectory of directories) {
364364
typeDirectory = normalizePath(typeDirectory);
365365
const directoryName = getBaseFileName(typeDirectory);
366-
const moduleName = getUnmangledNameForScopedPackage(directoryName);
366+
const moduleName = unmangleScopedPackageName(directoryName);
367367
pushResult(moduleName);
368368
}
369369
}

tests/baselines/reference/duplicatePackage_relativeImportWithinPackage.trace.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"======== Resolving module 'foo/use' from '/index.ts'. ========",
33
"Module resolution kind is not specified, using 'NodeJs'.",
44
"Loading module 'foo/use' from 'node_modules' folder, target file type 'TypeScript'.",
5+
"'package.json' does not have a 'typesVersions' field.",
56
"Found 'package.json' at '/node_modules/foo/package.json'. Package ID is 'foo/use/[email protected]'.",
67
"File '/node_modules/foo/use.ts' does not exist.",
78
"File '/node_modules/foo/use.tsx' does not exist.",
@@ -26,11 +27,13 @@
2627
"File '/node_modules/foo/index.ts' does not exist.",
2728
"File '/node_modules/foo/index.tsx' does not exist.",
2829
"File '/node_modules/foo/index.d.ts' exist - use it as a name resolution result.",
30+
"'package.json' does not have a 'typesVersions' field.",
2931
"Found 'package.json' at '/node_modules/foo/package.json'. Package ID is 'foo/[email protected]'.",
3032
"======== Module name './index' was successfully resolved to '/node_modules/foo/index.d.ts'. ========",
3133
"======== Resolving module 'foo' from '/node_modules/a/index.d.ts'. ========",
3234
"Module resolution kind is not specified, using 'NodeJs'.",
3335
"Loading module 'foo' from 'node_modules' folder, target file type 'TypeScript'.",
36+
"'package.json' does not have a 'typesVersions' field.",
3437
"'package.json' does not have a 'typings' field.",
3538
"'package.json' does not have a 'types' field.",
3639
"'package.json' does not have a 'main' field.",

0 commit comments

Comments
 (0)