Almost working?

This commit is contained in:
Ryan Cavanaugh
2018-06-10 19:28:38 -07:00
parent 19fe86a8c1
commit ab10b86205
74 changed files with 377 additions and 273 deletions

351
src/jsTyping/jsTyping.ts Normal file
View File

@@ -0,0 +1,351 @@
/* @internal */
namespace ts.JsTyping {
export interface TypingResolutionHost {
directoryExists(path: string): boolean;
fileExists(fileName: string): boolean;
readFile(path: string, encoding?: string): string | undefined;
readDirectory(rootDir: string, extensions: ReadonlyArray<string>, excludes: ReadonlyArray<string> | undefined, includes: ReadonlyArray<string> | undefined, depth?: number): string[];
}
interface PackageJson {
_requiredBy?: string[];
dependencies?: MapLike<string>;
devDependencies?: MapLike<string>;
name?: string;
optionalDependencies?: MapLike<string>;
peerDependencies?: MapLike<string>;
types?: string;
typings?: string;
}
export interface CachedTyping {
typingLocation: string;
version: Semver;
}
/* @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);
}
/* @internal */
export const nodeCoreModuleList: ReadonlyArray<string> = [
"assert",
"async_hooks",
"buffer",
"child_process",
"cluster",
"console",
"constants",
"crypto",
"dgram",
"dns",
"domain",
"events",
"fs",
"http",
"https",
"http2",
"inspector",
"net",
"os",
"path",
"perf_hooks",
"process",
"punycode",
"querystring",
"readline",
"repl",
"stream",
"string_decoder",
"timers",
"tls",
"tty",
"url",
"util",
"v8",
"vm",
"zlib"
];
/* @internal */
export const nodeCoreModules = arrayToSet(nodeCoreModuleList);
/**
* A map of loose file names to library names that we are confident require typings
*/
export type SafeList = ReadonlyMap<string>;
export function loadSafeList(host: TypingResolutionHost, safeListPath: Path): SafeList {
const result = readConfigFile(safeListPath, path => host.readFile(path));
return createMapFromTemplate<string>(result.config);
}
export function loadTypesMap(host: TypingResolutionHost, typesMapPath: Path): SafeList | undefined {
const result = readConfigFile(typesMapPath, path => host.readFile(path));
if (result.config) {
return createMapFromTemplate<string>(result.config.simpleMap);
}
return undefined;
}
/**
* @param host is the object providing I/O related operations.
* @param fileNames are the file names that belong to the same project
* @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 and installed versions
* @param typeAcquisition is used to customize the typing acquisition process
* @param compilerOptions are used as a source for typing inference
*/
export function discoverTypings(
host: TypingResolutionHost,
log: ((message: string) => void) | undefined,
fileNames: string[],
projectRootPath: Path,
safeList: SafeList,
packageNameToTypingLocation: ReadonlyMap<CachedTyping>,
typeAcquisition: TypeAcquisition,
unresolvedImports: ReadonlyArray<string>,
typesRegistry: ReadonlyMap<MapLike<string>>):
{ cachedTypingPaths: string[], newTypingNames: string[], filesToWatch: string[] } {
if (!typeAcquisition || !typeAcquisition.enable) {
return { cachedTypingPaths: [], newTypingNames: [], filesToWatch: [] };
}
// A typing name to typing file path mapping
const inferredTypings = createMap<string>();
// Only infer typings for .js and .jsx files
fileNames = mapDefined(fileNames, fileName => {
const path = normalizePath(fileName);
if (hasJavaScriptFileExtension(path)) {
return path;
}
});
const filesToWatch: string[] = [];
if (typeAcquisition.include) addInferredTypings(typeAcquisition.include, "Explicitly included types");
const exclude = typeAcquisition.exclude || [];
// Directories to search for package.json, bower.json and other typing information
const possibleSearchDirs = arrayToSet(fileNames, getDirectoryPath);
possibleSearchDirs.set(projectRootPath, true);
possibleSearchDirs.forEach((_true, searchDir) => {
const packageJsonPath = combinePaths(searchDir, "package.json");
getTypingNamesFromJson(packageJsonPath, filesToWatch);
const bowerJsonPath = combinePaths(searchDir, "bower.json");
getTypingNamesFromJson(bowerJsonPath, filesToWatch);
const bowerComponentsPath = combinePaths(searchDir, "bower_components");
getTypingNamesFromPackagesFolder(bowerComponentsPath, filesToWatch);
const nodeModulesPath = combinePaths(searchDir, "node_modules");
getTypingNamesFromPackagesFolder(nodeModulesPath, filesToWatch);
});
getTypingNamesFromSourceFileNames(fileNames);
// add typings for unresolved imports
if (unresolvedImports) {
const module = deduplicate<string>(
unresolvedImports.map(moduleId => nodeCoreModules.has(moduleId) ? "node" : moduleId),
equateStringsCaseSensitive,
compareStringsCaseSensitive);
addInferredTypings(module, "Inferred typings from unresolved imports");
}
// Add the cached typing locations for inferred typings that are already installed
packageNameToTypingLocation.forEach((typing, name) => {
const registryEntry = typesRegistry.get(name);
if (inferredTypings.has(name) && inferredTypings.get(name) === undefined && registryEntry !== undefined && isTypingUpToDate(typing, registryEntry)) {
inferredTypings.set(name, typing.typingLocation);
}
});
// Remove typings that the user has added to the exclude list
for (const excludeTypingName of exclude) {
const didDelete = inferredTypings.delete(excludeTypingName);
if (didDelete && log) log(`Typing for ${excludeTypingName} is in exclude list, will be ignored.`);
}
const newTypingNames: string[] = [];
const cachedTypingPaths: string[] = [];
inferredTypings.forEach((inferred, typing) => {
if (inferred !== undefined) {
cachedTypingPaths.push(inferred);
}
else {
newTypingNames.push(typing);
}
});
const result = { cachedTypingPaths, newTypingNames, filesToWatch };
if (log) log(`Result: ${JSON.stringify(result)}`);
return result;
function addInferredTyping(typingName: string) {
if (!inferredTypings.has(typingName)) {
inferredTypings.set(typingName, undefined!); // TODO: GH#18217
}
}
function addInferredTypings(typingNames: ReadonlyArray<string>, message: string) {
if (log) log(`${message}: ${JSON.stringify(typingNames)}`);
forEach(typingNames, addInferredTyping);
}
/**
* Get the typing info from common package manager json files like package.json or bower.json
*/
function getTypingNamesFromJson(jsonPath: string, filesToWatch: Push<string>) {
if (!host.fileExists(jsonPath)) {
return;
}
filesToWatch.push(jsonPath);
const jsonConfig: PackageJson = readConfigFile(jsonPath, path => host.readFile(path)).config;
const jsonTypingNames = flatMap([jsonConfig.dependencies, jsonConfig.devDependencies, jsonConfig.optionalDependencies, jsonConfig.peerDependencies], getOwnKeys);
addInferredTypings(jsonTypingNames, `Typing names in '${jsonPath}' dependencies`);
}
/**
* 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 fromFileNames = mapDefined(fileNames, j => {
if (!hasJavaScriptFileExtension(j)) return undefined;
const inferredTypingName = removeFileExtension(getBaseFileName(j.toLowerCase()));
const cleanedTypingName = removeMinAndVersionNumbers(inferredTypingName);
return safeList.get(cleanedTypingName);
});
if (fromFileNames.length) {
addInferredTypings(fromFileNames, "Inferred typings from file names");
}
const hasJsxFile = some(fileNames, f => fileExtensionIs(f, Extension.Jsx));
if (hasJsxFile) {
if (log) log(`Inferred 'react' typings due to presence of '.jsx' extension`);
addInferredTyping("react");
}
}
/**
* Infer typing names from packages folder (ex: node_module, bower_components)
* @param packagesFolderPath is the path to the packages folder
*/
function getTypingNamesFromPackagesFolder(packagesFolderPath: string, filesToWatch: Push<string>) {
filesToWatch.push(packagesFolderPath);
// Todo: add support for ModuleResolutionHost too
if (!host.directoryExists(packagesFolderPath)) {
return;
}
// depth of 2, so we access `node_modules/foo` but not `node_modules/foo/bar`
const fileNames = host.readDirectory(packagesFolderPath, [Extension.Json], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2);
if (log) log(`Searching for typing names in ${packagesFolderPath}; all files: ${JSON.stringify(fileNames)}`);
const packageNames: string[] = [];
for (const fileName of fileNames) {
const normalizedFileName = normalizePath(fileName);
const baseFileName = getBaseFileName(normalizedFileName);
if (baseFileName !== "package.json" && baseFileName !== "bower.json") {
continue;
}
const result = readConfigFile(normalizedFileName, (path: string) => host.readFile(path));
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 (baseFileName === "package.json" && 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;
}
const ownTypes = packageJson.types || packageJson.typings;
if (ownTypes) {
const absolutePath = getNormalizedAbsolutePath(ownTypes, getDirectoryPath(normalizedFileName));
if (log) log(` Package '${packageJson.name}' provides its own types.`);
inferredTypings.set(packageJson.name, absolutePath);
}
else {
packageNames.push(packageJson.name);
}
}
addInferredTypings(packageNames, " Found package names");
}
}
export const enum PackageNameValidationResult {
Ok,
ScopedPackagesNotSupported,
EmptyName,
NameTooLong,
NameStartsWithDot,
NameStartsWithUnderscore,
NameContainsNonURISafeCharacters
}
const maxPackageNameLength = 214;
/**
* Validates package name using rules defined at https://docs.npmjs.com/files/package.json
*/
export function validatePackageName(packageName: string): PackageNameValidationResult {
if (!packageName) {
return PackageNameValidationResult.EmptyName;
}
if (packageName.length > maxPackageNameLength) {
return PackageNameValidationResult.NameTooLong;
}
if (packageName.charCodeAt(0) === CharacterCodes.dot) {
return PackageNameValidationResult.NameStartsWithDot;
}
if (packageName.charCodeAt(0) === CharacterCodes._) {
return PackageNameValidationResult.NameStartsWithUnderscore;
}
// check if name is scope package like: starts with @ and has one '/' in the middle
// scoped packages are not currently supported
// TODO: when support will be added we'll need to split and check both scope and package name
if (/^@[^/]+\/[^/]+$/.test(packageName)) {
return PackageNameValidationResult.ScopedPackagesNotSupported;
}
if (encodeURIComponent(packageName) !== packageName) {
return PackageNameValidationResult.NameContainsNonURISafeCharacters;
}
return PackageNameValidationResult.Ok;
}
export function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string {
switch (result) {
case PackageNameValidationResult.EmptyName:
return `Package name '${typing}' cannot be empty`;
case PackageNameValidationResult.NameTooLong:
return `Package name '${typing}' should be less than ${maxPackageNameLength} characters`;
case PackageNameValidationResult.NameStartsWithDot:
return `Package name '${typing}' cannot start with '.'`;
case PackageNameValidationResult.NameStartsWithUnderscore:
return `Package name '${typing}' cannot start with '_'`;
case PackageNameValidationResult.ScopedPackagesNotSupported:
return `Package '${typing}' is scoped and currently is not supported`;
case PackageNameValidationResult.NameContainsNonURISafeCharacters:
return `Package name '${typing}' contains non URI safe characters`;
case PackageNameValidationResult.Ok:
return Debug.fail(); // Shouldn't have called this.
default:
throw Debug.assertNever(result);
}
}
}

61
src/jsTyping/semver.ts Normal file
View File

@@ -0,0 +1,61 @@
/* @internal */
namespace ts {
function stringToInt(str: string): number {
const n = parseInt(str, 10);
if (isNaN(n)) {
throw new Error(`Error in parseInt(${JSON.stringify(str)})`);
}
return n;
}
const isPrereleaseRegex = /^(.*)-next.\d+/;
const prereleaseSemverRegex = /^(\d+)\.(\d+)\.0-next.(\d+)$/;
const semverRegex = /^(\d+)\.(\d+)\.(\d+)$/;
export class Semver {
static parse(semver: string): Semver {
const isPrerelease = isPrereleaseRegex.test(semver);
const result = Semver.tryParse(semver, isPrerelease);
if (!result) {
throw new Error(`Unexpected semver: ${semver} (isPrerelease: ${isPrerelease})`);
}
return result;
}
static fromRaw({ major, minor, patch, isPrerelease }: Semver): Semver {
return new Semver(major, minor, patch, isPrerelease);
}
// This must parse the output of `versionString`.
private static tryParse(semver: string, isPrerelease: boolean): Semver | undefined {
// Per the semver spec <http://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."
const rgx = isPrerelease ? prereleaseSemverRegex : semverRegex;
const match = rgx.exec(semver);
return match ? new Semver(stringToInt(match[1]), stringToInt(match[2]), stringToInt(match[3]), isPrerelease) : undefined;
}
private constructor(
readonly major: number, readonly minor: number, readonly patch: number,
/**
* If true, this is `major.minor.0-next.patch`.
* If false, this is `major.minor.patch`.
*/
readonly isPrerelease: boolean) { }
get versionString(): string {
return this.isPrerelease ? `${this.major}.${this.minor}.0-next.${this.patch}` : `${this.major}.${this.minor}.${this.patch}`;
}
equals(sem: Semver): boolean {
return this.major === sem.major && this.minor === sem.minor && this.patch === sem.patch && this.isPrerelease === sem.isPrerelease;
}
greaterThan(sem: Semver): boolean {
return this.major > sem.major || this.major === sem.major
&& (this.minor > sem.minor || this.minor === sem.minor
&& (!this.isPrerelease && sem.isPrerelease || this.isPrerelease === sem.isPrerelease
&& this.patch > sem.patch));
}
}
}

41
src/jsTyping/shared.ts Normal file
View File

@@ -0,0 +1,41 @@
namespace ts.server {
// tslint:disable variable-name
export const ActionSet: ActionSet = "action::set";
export const ActionInvalidate: ActionInvalidate = "action::invalidate";
export const ActionPackageInstalled: ActionPackageInstalled = "action::packageInstalled";
export const EventTypesRegistry: EventTypesRegistry = "event::typesRegistry";
export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes";
export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes";
export const EventInitializationFailed: EventInitializationFailed = "event::initializationFailed";
export namespace Arguments {
export const GlobalCacheLocation = "--globalTypingsCacheLocation";
export const LogFile = "--logFile";
export const EnableTelemetry = "--enableTelemetry";
export const TypingSafeListLocation = "--typingSafeListLocation";
export const TypesMapLocation = "--typesMapLocation";
/**
* This argument specifies the location of the NPM executable.
* typingsInstaller will run the command with `${npmLocation} install ...`.
*/
export const NpmLocation = "--npmLocation";
}
export function hasArgument(argumentName: string) {
return sys.args.indexOf(argumentName) >= 0;
}
export function findArgument(argumentName: string): string | undefined {
const index = sys.args.indexOf(argumentName);
return index >= 0 && index < sys.args.length - 1
? sys.args[index + 1]
: undefined;
}
/*@internal*/
export function nowString() {
// E.g. "12:34:56.789"
const d = new Date();
return `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}.${d.getMilliseconds()}`;
}
}

View File

@@ -0,0 +1,23 @@
{
"extends": "../tsconfig-base",
"compilerOptions": {
"outFile": "../../built/local/jsTyping.js",
"types": [
"node"
],
"lib": [
"es6",
"scripthost"
]
},
"references": [
{ "path": "../core" },
{ "path": "../parser" }
],
"files": [
"shared.ts",
"types.ts",
"jsTyping.ts",
"semver.ts"
]
}

110
src/jsTyping/types.ts Normal file
View File

@@ -0,0 +1,110 @@
declare namespace ts.server {
export type ActionSet = "action::set";
export type ActionInvalidate = "action::invalidate";
export type ActionPackageInstalled = "action::packageInstalled";
export type EventTypesRegistry = "event::typesRegistry";
export type EventBeginInstallTypes = "event::beginInstallTypes";
export type EventEndInstallTypes = "event::endInstallTypes";
export type EventInitializationFailed = "event::initializationFailed";
export interface SortedReadonlyArray<T> extends ReadonlyArray<T> {
" __sortedArrayBrand": any;
}
export interface TypingInstallerResponse {
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | ActionPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
}
export interface TypingInstallerRequestWithProjectName {
readonly projectName: string;
}
/* @internal */
export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest;
export interface DiscoverTypings extends TypingInstallerRequestWithProjectName {
readonly fileNames: string[];
readonly projectRootPath: Path;
readonly compilerOptions: CompilerOptions;
readonly typeAcquisition: TypeAcquisition;
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly cachePath?: string;
readonly kind: "discover";
}
export interface CloseProject extends TypingInstallerRequestWithProjectName {
readonly kind: "closeProject";
}
export interface TypesRegistryRequest {
readonly kind: "typesRegistry";
}
export interface InstallPackageRequest extends TypingInstallerRequestWithProjectName {
readonly kind: "installPackage";
readonly fileName: Path;
readonly packageName: string;
readonly projectRootPath: Path;
}
/* @internal */
export interface TypesRegistryResponse extends TypingInstallerResponse {
readonly kind: EventTypesRegistry;
readonly typesRegistry: MapLike<MapLike<string>>;
}
export interface PackageInstalledResponse extends ProjectResponse {
readonly kind: ActionPackageInstalled;
readonly success: boolean;
readonly message: string;
}
export interface InitializationFailedResponse extends TypingInstallerResponse {
readonly kind: EventInitializationFailed;
readonly message: string;
}
export interface ProjectResponse extends TypingInstallerResponse {
readonly projectName: string;
}
export interface InvalidateCachedTypings extends ProjectResponse {
readonly kind: ActionInvalidate;
}
export interface InstallTypes extends ProjectResponse {
readonly kind: EventBeginInstallTypes | EventEndInstallTypes;
readonly eventId: number;
readonly typingsInstallerVersion: string;
readonly packagesToInstall: ReadonlyArray<string>;
}
export interface BeginInstallTypes extends InstallTypes {
readonly kind: EventBeginInstallTypes;
}
export interface EndInstallTypes extends InstallTypes {
readonly kind: EventEndInstallTypes;
readonly installSuccess: boolean;
}
/* @internal */
export interface InstallTypingHost extends JsTyping.TypingResolutionHost {
useCaseSensitiveFileNames: boolean;
writeFile(path: string, content: string): void;
createDirectory(path: string): void;
watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher;
watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher;
}
export interface SetTypings extends ProjectResponse {
readonly typeAcquisition: TypeAcquisition;
readonly compilerOptions: CompilerOptions;
readonly typings: string[];
readonly unresolvedImports: SortedReadonlyArray<string>;
readonly kind: ActionSet;
}
/* @internal */
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse;
}