Skip to content

Adds support for "typesVersions" redirects #26568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Sep 7, 2018
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2255,7 +2255,7 @@ namespace ts {
? 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
: Diagnostics.Try_npm_install_types_Slash_1_if_it_exists_or_add_a_new_declaration_d_ts_file_containing_declare_module_0,
packageId.name,
getMangledNameForScopedPackage(packageId.name))
mangleScopedPackageName(packageId.name))
: undefined;
errorOrSuggestion(isError, errorNode, chainDiagnosticMessages(
errorInfo,
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3273,7 +3273,7 @@
"category": "Message",
"code": 6104
},
"Expected type of '{0}' field in 'package.json' to be 'string', got '{1}'.": {
"Expected type of '{0}' field in 'package.json' to be '{1}', got '{2}'.": {
"category": "Message",
"code": 6105
},
Expand Down Expand Up @@ -3671,6 +3671,14 @@
"category": "Error",
"code": 6205
},
"'package.json' has invalid version '{0}' in 'typesVersions' field.": {
"category": "Message",
"code": 6206
},
"'package.json' does not have a 'typesVersions' entry that matches version '{0}'.": {
"category": "Message",
"code": 6207
},

"Projects to reference": {
"category": "Message",
Expand Down
169 changes: 122 additions & 47 deletions src/compiler/moduleNameResolver.ts

Large diffs are not rendered by default.

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

function getDirectoryOrExtensionlessFileName(path: string): string {
// If the file is the main module, it can be imported by the package name
Expand Down
155 changes: 155 additions & 0 deletions src/compiler/semver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/* @internal */
namespace ts {
// https://semver.org/#spec-item-2
// > A normal version number MUST take the form X.Y.Z where X, Y, and Z are non-negative
// > integers, and MUST NOT contain leading zeroes. X is the major version, Y is the minor
// > version, and Z is the patch version. Each element MUST increase numerically.
//
// NOTE: We differ here in that we allow X and X.Y, with missing parts having the default
// value of `0`.
const versionRegExp = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*)(?:\-([a-z0-9-.]+))?(?:\+([a-z0-9-.]+))?)?)?$/i;

// https://semver.org/#spec-item-9
// > A pre-release version MAY be denoted by appending a hyphen and a series of dot separated
// > identifiers immediately following the patch version. Identifiers MUST comprise only ASCII
// > alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers
// > MUST NOT include leading zeroes.
const prereleaseRegExp = /^(?:0|[1-9]\d*|[a-z-][a-z0-9-]*)(?:\.(?:0|[1-9]\d*|[a-z-][a-z0-9-]*))*$/i;

// https://semver.org/#spec-item-10
// > Build metadata MAY be denoted by appending a plus sign and a series of dot separated
// > identifiers immediately following the patch or pre-release version. Identifiers MUST
// > comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
const buildRegExp = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/i;

// https://semver.org/#spec-item-9
// > Numeric identifiers MUST NOT include leading zeroes.
const numericIdentifierRegExp = /^(0|[1-9]\d*)$/;

/**
* Describes a precise semantic version number, https://semver.org
*/
export class Version {
readonly major: number;
readonly minor: number;
readonly patch: number;
readonly prerelease: ReadonlyArray<string>;
readonly build: ReadonlyArray<string>;

constructor(text: string);
constructor(major: number, minor?: number, patch?: number, prerelease?: string, build?: string);
constructor(major: number | string, minor = 0, patch = 0, prerelease = "", build = "") {
if (typeof major === "string") {
const result = Debug.assertDefined(tryParseComponents(major), "Invalid version");
({ major, minor, patch, prerelease, build } = result);
}

Debug.assert(major >= 0, "Invalid argument: major");
Debug.assert(minor >= 0, "Invalid argument: minor");
Debug.assert(patch >= 0, "Invalid argument: patch");
Debug.assert(!prerelease || prereleaseRegExp.test(prerelease), "Invalid argument: prerelease");
Debug.assert(!build || buildRegExp.test(build), "Invalid argument: build");
this.major = major;
this.minor = minor;
this.patch = patch;
this.prerelease = prerelease ? prerelease.split(".") : emptyArray;
this.build = build ? build.split(".") : emptyArray;
}

static tryParse(text: string) {
const result = tryParseComponents(text);
if (!result) return undefined;

const { major, minor, patch, prerelease, build } = result;
return new Version(major, minor, patch, prerelease, build);
}

compareTo(other: Version | undefined) {
// https://semver.org/#spec-item-11
// > Precedence is determined by the first difference when comparing each of these
// > identifiers from left to right as follows: Major, minor, and patch versions are
// > always compared numerically.
//
// https://semver.org/#spec-item-11
// > Precedence for two pre-release versions with the same major, minor, and patch version
// > MUST be determined by comparing each dot separated identifier from left to right until
// > a difference is found [...]
//
// https://semver.org/#spec-item-11
// > Build metadata does not figure into precedence
if (this === other) return Comparison.EqualTo;
if (other === undefined) return Comparison.GreaterThan;
return compareValues(this.major, other.major)
|| compareValues(this.minor, other.minor)
|| compareValues(this.patch, other.patch)
|| comparePrerelaseIdentifiers(this.prerelease, other.prerelease);
}

toString() {
let result = `${this.major}.${this.minor}.${this.patch}`;
if (some(this.prerelease)) result += `-${this.prerelease.join(".")}`;
if (some(this.build)) result += `+${this.build.join(".")}`;
return result;
}
}

function tryParseComponents(text: string) {
const match = versionRegExp.exec(text);
if (!match) return undefined;

const [, major, minor = "0", patch = "0", prerelease = "", build = ""] = match;
if (prerelease && !prereleaseRegExp.test(prerelease)) return undefined;
if (build && !buildRegExp.test(build)) return undefined;
return {
major: parseInt(major, 10),
minor: parseInt(minor, 10),
patch: parseInt(patch, 10),
prerelease,
build
};
}

function comparePrerelaseIdentifiers(left: ReadonlyArray<string>, right: ReadonlyArray<string>) {
// https://semver.org/#spec-item-11
// > When major, minor, and patch are equal, a pre-release version has lower precedence
// > than a normal version.
if (left === right) return Comparison.EqualTo;
if (left.length === 0) return right.length === 0 ? Comparison.EqualTo : Comparison.GreaterThan;
if (right.length === 0) return Comparison.LessThan;

// https://semver.org/#spec-item-11
// > Precedence for two pre-release versions with the same major, minor, and patch version
// > MUST be determined by comparing each dot separated identifier from left to right until
// > a difference is found [...]
const length = Math.min(left.length, right.length);
for (let i = 0; i < length; i++) {
const leftIdentifier = left[i];
const rightIdentifier = right[i];
if (leftIdentifier === rightIdentifier) continue;

const leftIsNumeric = numericIdentifierRegExp.test(leftIdentifier);
const rightIsNumeric = numericIdentifierRegExp.test(rightIdentifier);
if (leftIsNumeric || rightIsNumeric) {
// https://semver.org/#spec-item-11
// > Numeric identifiers always have lower precedence than non-numeric identifiers.
if (leftIsNumeric !== rightIsNumeric) return leftIsNumeric ? Comparison.LessThan : Comparison.GreaterThan;

// https://semver.org/#spec-item-11
// > identifiers consisting of only digits are compared numerically
const result = compareValues(+leftIdentifier, +rightIdentifier);
if (result) return result;
}
else {
// https://semver.org/#spec-item-11
// > identifiers with letters or hyphens are compared lexically in ASCII sort order.
const result = compareStringsCaseSensitive(leftIdentifier, rightIdentifier);
if (result) return result;
}
}

// https://semver.org/#spec-item-11
// > A larger set of pre-release fields has a higher precedence than a smaller set, if all
// > of the preceding identifiers are equal.
return compareValues(left.length, right.length);
}
}
1 change: 1 addition & 0 deletions src/compiler/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"files": [
"core.ts",
"performance.ts",
"semver.ts",

"types.ts",
"sys.ts",
Expand Down
21 changes: 21 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3940,6 +3940,27 @@ namespace ts {
return getStringFromExpandedCharCodes(expandedCharCodes);
}

export function readJson(path: string, host: { readFile(fileName: string): string | undefined }): object {
try {
const jsonText = host.readFile(path);
if (!jsonText) return {};
const result = parseConfigFileTextToJson(path, jsonText);
if (result.error) {
return {};
}
return result.config;
}
catch (e) {
// gracefully handle if readFile fails or returns not JSON
return {};
}
}

export function directoryProbablyExists(directoryName: string, host: { directoryExists?: (directoryName: string) => boolean }): boolean {
// if host does not support 'directoryExists' assume that directory will exist
return !host.directoryExists || host.directoryExists(directoryName);
}

const carriageReturnLineFeed = "\r\n";
const lineFeed = "\n";
export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, getNewLine?: () => string): string {
Expand Down
6 changes: 3 additions & 3 deletions src/jsTyping/jsTyping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ namespace ts.JsTyping {

export interface CachedTyping {
typingLocation: string;
version: Semver;
version: Version;
}

/* @internal */
export function isTypingUpToDate(cachedTyping: CachedTyping, availableTypingVersions: MapLike<string>) {
const availableVersion = Semver.parse(getProperty(availableTypingVersions, `ts${versionMajorMinor}`) || getProperty(availableTypingVersions, "latest")!);
return !availableVersion.greaterThan(cachedTyping.version);
const availableVersion = new Version(getProperty(availableTypingVersions, `ts${versionMajorMinor}`) || getProperty(availableTypingVersions, "latest")!);
return availableVersion.compareTo(cachedTyping.version) <= 0;
}

/* @internal */
Expand Down
61 changes: 0 additions & 61 deletions src/jsTyping/semver.ts

This file was deleted.

3 changes: 1 addition & 2 deletions src/jsTyping/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"files": [
"shared.ts",
"types.ts",
"jsTyping.ts",
"semver.ts"
"jsTyping.ts"
]
}
2 changes: 1 addition & 1 deletion src/services/codefixes/fixCannotFindModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ namespace ts.codefix {

function getTypesPackageNameToInstall(host: LanguageServiceHost, sourceFile: SourceFile, pos: number, diagCode: number): string | undefined {
const moduleName = cast(getTokenAtPosition(sourceFile, pos), isStringLiteral).text;
const { packageName } = getPackageName(moduleName);
const { packageName } = parsePackageName(moduleName);
return diagCode === errorCodeCannotFindModule
? (JsTyping.nodeCoreModules.has(packageName) ? "@types/node" : undefined)
: (host.isKnownTypesPackageName!(packageName) ? getTypesPackageName(packageName) : undefined); // TODO: GH#18217
Expand Down
32 changes: 29 additions & 3 deletions src/services/pathCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,24 @@ namespace ts.Completions.PathCompletions {

// const absolutePath = normalizeAndPreserveTrailingSlash(isRootedDiskPath(fragment) ? fragment : combinePaths(scriptPath, fragment)); // TODO(rbuckton): should use resolvePaths
const absolutePath = resolvePath(scriptPath, fragment);
const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
let baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());

if (tryDirectoryExists(host, baseDirectory)) {
// check for a version redirect
const packageJsonPath = findPackageJson(baseDirectory, host);
if (packageJsonPath) {
const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined });
const typesVersions = (packageJson as any).typesVersions;
if (typeof typesVersions === "object") {
const result = getPackageJsonTypesVersionsOverride(typesVersions);
const versionPath = result && result.directory;
if (versionPath) {
baseDirectory = resolvePath(baseDirectory, versionPath);
}
}
}

// Enumerate the available files if possible
const files = tryReadDirectory(host, baseDirectory, extensions, /*exclude*/ undefined, /*include*/ ["./*"]);

Expand Down Expand Up @@ -329,7 +343,7 @@ namespace ts.Completions.PathCompletions {
const seen = createMap<true>();
if (options.types) {
for (const typesName of options.types) {
const moduleName = getUnmangledNameForScopedPackage(typesName);
const moduleName = unmangleScopedPackageName(typesName);
pushResult(moduleName);
}
}
Expand Down Expand Up @@ -363,7 +377,7 @@ namespace ts.Completions.PathCompletions {
for (let typeDirectory of directories) {
typeDirectory = normalizePath(typeDirectory);
const directoryName = getBaseFileName(typeDirectory);
const moduleName = getUnmangledNameForScopedPackage(directoryName);
const moduleName = unmangleScopedPackageName(directoryName);
pushResult(moduleName);
}
}
Expand All @@ -390,6 +404,18 @@ namespace ts.Completions.PathCompletions {
return paths;
}

function findPackageJson(directory: string, host: LanguageServiceHost): string | undefined {
let packageJson: string | undefined;
forEachAncestorDirectory(directory, ancestor => {
if (ancestor === "node_modules") return true;
packageJson = findConfigFile(ancestor, (f) => tryFileExists(host, f), "package.json");
if (packageJson) {
return true; // break out
}
});
return packageJson;
}

function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): ReadonlyArray<string> {
if (!host.readFile || !host.fileExists) return emptyArray;

Expand Down
Loading