Merge pull request #7179 from Microsoft/jsTypingForAcquireDts

Salsa: JS support for discovering and acquiring d.ts files
This commit is contained in:
jramsay
2016-03-02 11:10:12 -08:00
11 changed files with 391 additions and 51 deletions
+1
View File
@@ -927,6 +927,7 @@ var servicesLintTargets = [
"patternMatcher.ts",
"services.ts",
"shims.ts",
"jsTyping.ts"
].map(function (s) {
return path.join(servicesDirectory, s);
});
+55 -22
View File
@@ -543,6 +543,7 @@ namespace ts {
return {
options,
fileNames: getFileNames(),
typingOptions: getTypingOptions(),
errors
};
@@ -607,6 +608,35 @@ namespace ts {
}
return fileNames;
}
function getTypingOptions(): TypingOptions {
const options: TypingOptions = getBaseFileName(configFileName) === "jsconfig.json"
? { enableAutoDiscovery: true, include: [], exclude: [] }
: { enableAutoDiscovery: false, include: [], exclude: [] };
const jsonTypingOptions = json["typingOptions"];
if (jsonTypingOptions) {
for (const id in jsonTypingOptions) {
if (id === "enableAutoDiscovery") {
if (typeof jsonTypingOptions[id] === "boolean") {
options.enableAutoDiscovery = jsonTypingOptions[id];
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Unknown_typing_option_0, id));
}
}
else if (id === "include") {
options.include = convertJsonOptionToStringArray(id, jsonTypingOptions[id], errors);
}
else if (id === "exclude") {
options.exclude = convertJsonOptionToStringArray(id, jsonTypingOptions[id], errors);
}
else {
errors.push(createCompilerDiagnostic(Diagnostics.Unknown_typing_option_0, id));
}
}
}
return options;
}
}
export function convertCompilerOptionsFromJson(jsonOptions: any, basePath: string, configFileName?: string): { options: CompilerOptions, errors: Diagnostic[] } {
@@ -647,28 +677,7 @@ namespace ts {
break;
case "object":
// "object" options with 'isFilePath' = true expected to be string arrays
let paths: string[] = [];
let invalidOptionType = false;
if (!isArray(value)) {
invalidOptionType = true;
}
else {
for (const element of <any[]>value) {
if (typeof element === "string") {
paths.push(normalizePath(combinePaths(basePath, element)));
}
else {
invalidOptionType = true;
break;
}
}
}
if (invalidOptionType) {
errors.push(createCompilerDiagnostic(Diagnostics.Option_0_should_have_array_of_strings_as_a_value, opt.name));
}
else {
value = paths;
}
value = convertJsonOptionToStringArray(opt.name, value, errors, (element) => normalizePath(combinePaths(basePath, element)));
break;
}
if (value === "") {
@@ -688,4 +697,28 @@ namespace ts {
return { options, errors };
}
function convertJsonOptionToStringArray(optionName: string, optionJson: any, errors: Diagnostic[], func?: (element: string) => string): string[] {
const items: string[] = [];
let invalidOptionType = false;
if (!isArray(optionJson)) {
invalidOptionType = true;
}
else {
for (const element of <any[]>optionJson) {
if (typeof element === "string") {
const item = func ? func(element) : element;
items.push(item);
}
else {
invalidOptionType = true;
break;
}
}
}
if (invalidOptionType) {
errors.push(createCompilerDiagnostic(Diagnostics.Option_0_should_have_array_of_strings_as_a_value, optionName));
}
return items;
}
}
+34
View File
@@ -278,6 +278,14 @@ namespace ts {
return hasOwnProperty.call(map, key);
}
export function getKeys<T>(map: Map<T>): string[] {
const keys: string[] = [];
for (const key in map) {
keys.push(key);
}
return keys;
}
export function getProperty<T>(map: Map<T>, key: string): T {
return hasOwnProperty.call(map, key) ? map[key] : undefined;
}
@@ -778,6 +786,32 @@ namespace ts {
return pathLen > extLen && path.substr(pathLen - extLen, extLen) === extension;
}
export function ensureScriptKind(fileName: string, scriptKind?: ScriptKind): ScriptKind {
// Using scriptKind as a condition handles both:
// - 'scriptKind' is unspecified and thus it is `undefined`
// - 'scriptKind' is set and it is `Unknown` (0)
// If the 'scriptKind' is 'undefined' or 'Unknown' then we attempt
// to get the ScriptKind from the file name. If it cannot be resolved
// from the file name then the default 'TS' script kind is returned.
return (scriptKind || getScriptKindFromFileName(fileName)) || ScriptKind.TS;
}
export function getScriptKindFromFileName(fileName: string): ScriptKind {
const ext = fileName.substr(fileName.lastIndexOf("."));
switch (ext.toLowerCase()) {
case ".js":
return ScriptKind.JS;
case ".jsx":
return ScriptKind.JSX;
case ".ts":
return ScriptKind.TS;
case ".tsx":
return ScriptKind.TSX;
default:
return ScriptKind.Unknown;
}
}
/**
* List of supported extensions in order of file resolution precedence.
*/
+4
View File
@@ -2804,5 +2804,9 @@
"'super' must be called before accessing 'this' in the constructor of a derived class.": {
"category": "Error",
"code": 17009
},
"Unknown typing option '{0}'.": {
"category": "Error",
"code": 17010
}
}
+1 -23
View File
@@ -407,23 +407,6 @@ namespace ts {
return result;
}
/* @internal */
export function getScriptKindFromFileName(fileName: string): ScriptKind {
const ext = fileName.substr(fileName.lastIndexOf("."));
switch (ext.toLowerCase()) {
case ".js":
return ScriptKind.JS;
case ".jsx":
return ScriptKind.JSX;
case ".ts":
return ScriptKind.TS;
case ".tsx":
return ScriptKind.TSX;
default:
return ScriptKind.TS;
}
}
// Produces a new SourceFile for the 'newText' provided. The 'textChangeRange' parameter
// indicates what changed between the 'text' that this SourceFile has and the 'newText'.
// The SourceFile will be created with the compiler attempting to reuse as many nodes from
@@ -551,12 +534,7 @@ namespace ts {
let parseErrorBeforeNextFinishedNode = false;
export function parseSourceFile(fileName: string, _sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor, setParentNodes?: boolean, scriptKind?: ScriptKind): SourceFile {
// Using scriptKind as a condition handles both:
// - 'scriptKind' is unspecified and thus it is `undefined`
// - 'scriptKind' is set and it is `Unknown` (0)
// If the 'scriptKind' is 'undefined' or 'Unknown' then attempt
// to get the ScriptKind from the file name.
scriptKind = scriptKind ? scriptKind : getScriptKindFromFileName(fileName);
scriptKind = ensureScriptKind(fileName, scriptKind);
initializeState(fileName, _sourceText, languageVersion, _syntaxCursor, scriptKind);
+18
View File
@@ -2433,6 +2433,23 @@ namespace ts {
[option: string]: string | number | boolean | TsConfigOnlyOptions;
}
export interface TypingOptions {
enableAutoDiscovery?: boolean;
include?: string[];
exclude?: string[];
[option: string]: string[] | boolean;
}
export interface DiscoverTypingsInfo {
fileNames: string[]; // The file names that belong to the same project.
cachePath: string; // The path to the typings cache
projectRootPath: string; // The path to the project root directory
safeListPath: string; // The path used to retrieve the safe list
packageNameToTypingLocation: Map<string>; // The map of package names to their cached typing locations
typingOptions: TypingOptions; // Used to customize the typing inference process
compilerOptions: CompilerOptions; // Used as a source for typing inference
}
export enum ModuleKind {
None = 0,
CommonJS = 1,
@@ -2491,6 +2508,7 @@ namespace ts {
export interface ParsedCommandLine {
options: CompilerOptions;
typingOptions?: TypingOptions;
fileNames: string[];
errors: Diagnostic[];
}
+229
View File
@@ -0,0 +1,229 @@
// Copyright (c) Microsoft. All rights reserved. Licensed under the Apache License, Version 2.0.
// See LICENSE.txt in the project root for complete license information.
/// <reference path='services.ts' />
/* @internal */
namespace ts.JsTyping {
interface TypingResolutionHost {
directoryExists: (path: string) => boolean;
fileExists: (fileName: string) => boolean;
readFile: (path: string, encoding?: string) => string;
readDirectory: (path: string, extension?: string, exclude?: string[], depth?: number) => string[];
};
interface PackageJson {
_requiredBy?: string[];
dependencies?: Map<string>;
devDependencies?: Map<string>;
name?: string;
optionalDependencies?: Map<string>;
peerDependencies?: Map<string>;
typings?: string;
};
// A map of loose file names to library names
// that we are confident require typings
let safeList: Map<string>;
/**
* @param host is the object providing I/O related operations.
* @param fileNames are the file names that belong to the same project
* @param cachePath is the path to the typings cache
* @param projectRootPath is the path to the project root directory
* @param safeListPath is the path used to retrieve the safe list
* @param packageNameToTypingLocation is the map of package names to their cached typing locations
* @param typingOptions are used to customize the typing inference process
* @param compilerOptions are used as a source for typing inference
*/
export function discoverTypings(
host: TypingResolutionHost,
fileNames: string[],
cachePath: Path,
projectRootPath: Path,
safeListPath: Path,
packageNameToTypingLocation: Map<string>,
typingOptions: TypingOptions,
compilerOptions: CompilerOptions):
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
// A typing name to typing file path mapping
const inferredTypings: Map<string> = {};
if (!typingOptions || !typingOptions.enableAutoDiscovery) {
return { cachedTypingPaths: [], newTypingNames: [], filesToWatch: [] };
}
// Only infer typings for .js and .jsx files
fileNames = filter(map(fileNames, normalizePath), f => scriptKindIs(f, /*LanguageServiceHost*/ undefined, ScriptKind.JS, ScriptKind.JSX));
if (!safeList) {
const result = readConfigFile(safeListPath, (path: string) => host.readFile(path));
if (result.config) {
safeList = result.config;
}
else {
safeList = {};
};
}
const filesToWatch: string[] = [];
// Directories to search for package.json, bower.json and other typing information
let searchDirs: string[] = [];
let exclude: string[] = [];
mergeTypings(typingOptions.include);
exclude = typingOptions.exclude || [];
const possibleSearchDirs = map(fileNames, getDirectoryPath);
if (projectRootPath !== undefined) {
possibleSearchDirs.push(projectRootPath);
}
searchDirs = deduplicate(possibleSearchDirs);
for (const searchDir of searchDirs) {
const packageJsonPath = combinePaths(searchDir, "package.json");
getTypingNamesFromJson(packageJsonPath, filesToWatch);
const bowerJsonPath = combinePaths(searchDir, "bower.json");
getTypingNamesFromJson(bowerJsonPath, filesToWatch);
const nodeModulesPath = combinePaths(searchDir, "node_modules");
getTypingNamesFromNodeModuleFolder(nodeModulesPath);
}
getTypingNamesFromSourceFileNames(fileNames);
// Add the cached typing locations for inferred typings that are already installed
for (const name in packageNameToTypingLocation) {
if (hasProperty(inferredTypings, name) && !inferredTypings[name]) {
inferredTypings[name] = packageNameToTypingLocation[name];
}
}
// Remove typings that the user has added to the exclude list
for (const excludeTypingName of exclude) {
delete inferredTypings[excludeTypingName];
}
const newTypingNames: string[] = [];
const cachedTypingPaths: string[] = [];
for (const typing in inferredTypings) {
if (inferredTypings[typing] !== undefined) {
cachedTypingPaths.push(inferredTypings[typing]);
}
else {
newTypingNames.push(typing);
}
}
return { cachedTypingPaths, newTypingNames, filesToWatch };
/**
* Merge a given list of typingNames to the inferredTypings map
*/
function mergeTypings(typingNames: string[]) {
if (!typingNames) {
return;
}
for (const typing of typingNames) {
if (!hasProperty(inferredTypings, typing)) {
inferredTypings[typing] = undefined;
}
}
}
/**
* Get the typing info from common package manager json files like package.json or bower.json
*/
function getTypingNamesFromJson(jsonPath: string, filesToWatch: string[]) {
const result = readConfigFile(jsonPath, (path: string) => host.readFile(path));
if (result.config) {
const jsonConfig: PackageJson = result.config;
filesToWatch.push(jsonPath);
if (jsonConfig.dependencies) {
mergeTypings(getKeys(jsonConfig.dependencies));
}
if (jsonConfig.devDependencies) {
mergeTypings(getKeys(jsonConfig.devDependencies));
}
if (jsonConfig.optionalDependencies) {
mergeTypings(getKeys(jsonConfig.optionalDependencies));
}
if (jsonConfig.peerDependencies) {
mergeTypings(getKeys(jsonConfig.peerDependencies));
}
}
}
/**
* Infer typing names from given file names. For example, the file name "jquery-min.2.3.4.js"
* should be inferred to the 'jquery' typing name; and "angular-route.1.2.3.js" should be inferred
* to the 'angular-route' typing name.
* @param fileNames are the names for source files in the project
*/
function getTypingNamesFromSourceFileNames(fileNames: string[]) {
const jsFileNames = filter(fileNames, hasJavaScriptFileExtension);
const inferredTypingNames = map(jsFileNames, f => removeFileExtension(getBaseFileName(f.toLowerCase())));
const cleanedTypingNames = map(inferredTypingNames, f => f.replace(/((?:\.|-)min(?=\.|$))|((?:-|\.)\d+)/g, ""));
if (safeList === undefined) {
mergeTypings(cleanedTypingNames);
}
else {
mergeTypings(filter(cleanedTypingNames, f => hasProperty(safeList, f)));
}
const hasJsxFile = forEach(fileNames, f => scriptKindIs(f, /*LanguageServiceHost*/ undefined, ScriptKind.JSX));
if (hasJsxFile) {
mergeTypings(["react"]);
}
}
/**
* Infer typing names from node_module folder
* @param nodeModulesPath is the path to the "node_modules" folder
*/
function getTypingNamesFromNodeModuleFolder(nodeModulesPath: string) {
// Todo: add support for ModuleResolutionHost too
if (!host.directoryExists(nodeModulesPath)) {
return;
}
const typingNames: string[] = [];
const fileNames = host.readDirectory(nodeModulesPath, "*.json", /*exclude*/ undefined, /*depth*/ 2);
for (const fileName of fileNames) {
const normalizedFileName = normalizePath(fileName);
if (getBaseFileName(normalizedFileName) !== "package.json") {
continue;
}
const result = readConfigFile(normalizedFileName, (path: string) => host.readFile(path));
if (!result.config) {
continue;
}
const packageJson: PackageJson = result.config;
// npm 3's package.json contains a "_requiredBy" field
// we should include all the top level module names for npm 2, and only module names whose
// "_requiredBy" field starts with "#" or equals "/" for npm 3.
if (packageJson._requiredBy &&
filter(packageJson._requiredBy, (r: string) => r[0] === "#" || r === "/").length === 0) {
continue;
}
// If the package has its own d.ts typings, those will take precedence. Otherwise the package name will be used
// to download d.ts files from DefinitelyTyped
if (!packageJson.name) {
continue;
}
if (packageJson.typings) {
const absolutePath = getNormalizedAbsolutePath(packageJson.typings, getDirectoryPath(normalizedFileName));
inferredTypings[packageJson.name] = absolutePath;
}
else {
typingNames.push(packageJson.name);
}
}
mergeTypings(typingNames);
}
}
}
+3 -3
View File
@@ -7,6 +7,7 @@
/// <reference path='patternMatcher.ts' />
/// <reference path='signatureHelp.ts' />
/// <reference path='utilities.ts' />
/// <reference path='jsTyping.ts' />
/// <reference path='formatting\formatting.ts' />
/// <reference path='formatting\smartIndenter.ts' />
@@ -1751,14 +1752,13 @@ namespace ts {
private createEntry(fileName: string, path: Path) {
let entry: HostFileInformation;
const scriptKind = this.host.getScriptKind ? this.host.getScriptKind(fileName) : ScriptKind.Unknown;
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
if (scriptSnapshot) {
entry = {
hostFileName: fileName,
version: this.host.getScriptVersion(fileName),
scriptSnapshot: scriptSnapshot,
scriptKind: scriptKind ? scriptKind : getScriptKindFromFileName(fileName)
scriptKind: getScriptKind(fileName, this.host)
};
}
@@ -1824,7 +1824,7 @@ namespace ts {
throw new Error("Could not find file: '" + fileName + "'.");
}
const scriptKind = this.host.getScriptKind ? this.host.getScriptKind(fileName) : ScriptKind.Unknown;
const scriptKind = getScriptKind(fileName, this.host);
const version = this.host.getScriptVersion(fileName);
let sourceFile: SourceFile;
+30 -3
View File
@@ -78,7 +78,7 @@ namespace ts {
* @param exclude A JSON encoded string[] containing the paths to exclude
* when enumerating the directory.
*/
readDirectory(rootDir: string, extension: string, exclude?: string): string;
readDirectory(rootDir: string, extension: string, exclude?: string, depth?: number): string;
trace(s: string): void;
}
@@ -232,6 +232,7 @@ namespace ts {
getPreProcessedFileInfo(fileName: string, sourceText: IScriptSnapshot): string;
getTSConfigFileInfo(fileName: string, sourceText: IScriptSnapshot): string;
getDefaultCompilationSettings(): string;
discoverTypings(discoverTypingsJson: string): string;
}
function logInternalError(logger: Logger, err: Error) {
@@ -422,8 +423,16 @@ namespace ts {
}
}
public readDirectory(rootDir: string, extension: string, exclude: string[]): string[] {
const encoded = this.shimHost.readDirectory(rootDir, extension, JSON.stringify(exclude));
public readDirectory(rootDir: string, extension: string, exclude: string[], depth?: number): string[] {
// Wrap the API changes for 2.0 release. This try/catch
// should be removed once TypeScript 2.0 has shipped.
let encoded: string;
try {
encoded = this.shimHost.readDirectory(rootDir, extension, JSON.stringify(exclude), depth);
}
catch (e) {
encoded = this.shimHost.readDirectory(rootDir, extension, JSON.stringify(exclude));
}
return JSON.parse(encoded);
}
@@ -953,6 +962,7 @@ namespace ts {
if (result.error) {
return {
options: {},
typingOptions: {},
files: [],
errors: [realizeDiagnostic(result.error, "\r\n")]
};
@@ -963,6 +973,7 @@ namespace ts {
return {
options: configFile.options,
typingOptions: configFile.typingOptions,
files: configFile.fileNames,
errors: realizeDiagnostics(configFile.errors, "\r\n")
};
@@ -975,6 +986,22 @@ namespace ts {
() => getDefaultCompilerOptions()
);
}
public discoverTypings(discoverTypingsJson: string): string {
const getCanonicalFileName = createGetCanonicalFileName(/*useCaseSensitivefileNames:*/ false);
return this.forwardJSONCall("discoverTypings()", () => {
const info = <DiscoverTypingsInfo>JSON.parse(discoverTypingsJson);
return ts.JsTyping.discoverTypings(
this.host,
info.fileNames,
toPath(info.cachePath, info.cachePath, getCanonicalFileName),
toPath(info.projectRootPath, info.projectRootPath, getCanonicalFileName),
toPath(info.safeListPath, info.safeListPath, getCanonicalFileName),
info.packageNameToTypingLocation,
info.typingOptions,
info.compilerOptions);
});
}
}
export class TypeScriptServicesFactory implements ShimFactory {
+1
View File
@@ -29,6 +29,7 @@
"shims.ts",
"signatureHelp.ts",
"utilities.ts",
"jsTyping.ts",
"formatting/formatting.ts",
"formatting/formattingContext.ts",
"formatting/formattingRequestKind.ts",
+15
View File
@@ -837,4 +837,19 @@ namespace ts {
};
return name;
}
export function scriptKindIs(fileName: string, host: LanguageServiceHost, ...scriptKinds: ScriptKind[]): boolean {
const scriptKind = getScriptKind(fileName, host);
return forEach(scriptKinds, k => k === scriptKind);
}
export function getScriptKind(fileName: string, host?: LanguageServiceHost): ScriptKind {
// First check to see if the script kind can be determined from the file name
var scriptKind = getScriptKindFromFileName(fileName);
if (scriptKind === ScriptKind.Unknown && host && host.getScriptKind) {
// Next check to see if the host can resolve the script kind
scriptKind = host.getScriptKind(fileName);
}
return ensureScriptKind(fileName, scriptKind);
}
}