mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-27 13:42:16 -05:00
Add quickfix and refactoring to install @types packages (#19130)
* Add quickfix and refactoring to install @types packages * Move `validatePackageName` to `jsTyping.ts` * Remove combinePaths overloads * Respond to code review * Update api baselines * Use native PromiseConstructor * Return false instead of undefined * Remove getProjectRootPath * Update api
This commit is contained in:
@@ -14,7 +14,7 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function extractMessage(message: string) {
|
||||
export function extractMessage(message: string): string {
|
||||
// Read the content length
|
||||
const contentLengthPrefix = "Content-Length: ";
|
||||
const lines = message.split(/\r?\n/);
|
||||
@@ -542,6 +542,8 @@ namespace ts.server {
|
||||
return response.body.map(entry => this.convertCodeActions(entry, file));
|
||||
}
|
||||
|
||||
applyCodeActionCommand = notImplemented;
|
||||
|
||||
private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs {
|
||||
return typeof positionOrRange === "number"
|
||||
? this.createFileLocationRequestArgs(fileName, positionOrRange)
|
||||
|
||||
@@ -242,6 +242,16 @@ namespace ts.server {
|
||||
this.markAsDirty();
|
||||
}
|
||||
|
||||
isKnownTypesPackageName(name: string): boolean {
|
||||
return this.typingsCache.isKnownTypesPackageName(name);
|
||||
}
|
||||
installPackage(options: InstallPackageOptions): PromiseLike<ApplyCodeActionCommandResult> {
|
||||
return this.typingsCache.installPackage({ ...options, projectRootPath: this.toPath(this.currentDirectory) });
|
||||
}
|
||||
private get typingsCache(): TypingsCache {
|
||||
return this.projectService.typingsCache;
|
||||
}
|
||||
|
||||
// Method of LanguageServiceHost
|
||||
getCompilationSettings() {
|
||||
return this.compilerOptions;
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace ts.server.protocol {
|
||||
BreakpointStatement = "breakpointStatement",
|
||||
CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects",
|
||||
GetCodeFixes = "getCodeFixes",
|
||||
ApplyCodeActionCommand = "applyCodeActionCommand",
|
||||
/* @internal */
|
||||
GetCodeFixesFull = "getCodeFixes-full",
|
||||
GetSupportedCodeFixes = "getSupportedCodeFixes",
|
||||
@@ -125,6 +126,8 @@ namespace ts.server.protocol {
|
||||
* Client-initiated request message
|
||||
*/
|
||||
export interface Request extends Message {
|
||||
type: "request";
|
||||
|
||||
/**
|
||||
* The command to execute
|
||||
*/
|
||||
@@ -147,6 +150,8 @@ namespace ts.server.protocol {
|
||||
* Server-initiated event message
|
||||
*/
|
||||
export interface Event extends Message {
|
||||
type: "event";
|
||||
|
||||
/**
|
||||
* Name of event
|
||||
*/
|
||||
@@ -162,6 +167,8 @@ namespace ts.server.protocol {
|
||||
* Response by server to client request message.
|
||||
*/
|
||||
export interface Response extends Message {
|
||||
type: "response";
|
||||
|
||||
/**
|
||||
* Sequence number of the request message.
|
||||
*/
|
||||
@@ -178,7 +185,8 @@ namespace ts.server.protocol {
|
||||
command: string;
|
||||
|
||||
/**
|
||||
* Contains error message if success === false.
|
||||
* If success === false, this should always be provided.
|
||||
* Otherwise, may (or may not) contain a success message.
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
@@ -520,6 +528,14 @@ namespace ts.server.protocol {
|
||||
arguments: CodeFixRequestArgs;
|
||||
}
|
||||
|
||||
export interface ApplyCodeActionCommandRequest extends Request {
|
||||
command: CommandTypes.ApplyCodeActionCommand;
|
||||
arguments: ApplyCodeActionCommandRequestArgs;
|
||||
}
|
||||
|
||||
// All we need is the `success` and `message` fields of Response.
|
||||
export interface ApplyCodeActionCommandResponse extends Response {}
|
||||
|
||||
export interface FileRangeRequestArgs extends FileRequestArgs {
|
||||
/**
|
||||
* The line number for the request (1-based).
|
||||
@@ -564,6 +580,10 @@ namespace ts.server.protocol {
|
||||
errorCodes?: number[];
|
||||
}
|
||||
|
||||
export interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs {
|
||||
command: {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for GetCodeFixes request.
|
||||
*/
|
||||
@@ -1541,6 +1561,8 @@ namespace ts.server.protocol {
|
||||
description: string;
|
||||
/** Text changes to apply to each file as part of the code action */
|
||||
changes: FileCodeEdits[];
|
||||
/** A command is an opaque object that should be passed to `ApplyCodeActionCommandRequestArgs` without modification. */
|
||||
commands?: {}[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -250,6 +250,9 @@ namespace ts.server {
|
||||
private activeRequestCount = 0;
|
||||
private requestQueue: QueuedOperation[] = [];
|
||||
private requestMap = createMap<QueuedOperation>(); // Maps operation ID to newest requestQueue entry with that ID
|
||||
/** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */
|
||||
private requestedRegistry: boolean;
|
||||
private typesRegistryCache: Map<void> | undefined;
|
||||
|
||||
// This number is essentially arbitrary. Processing more than one typings request
|
||||
// at a time makes sense, but having too many in the pipe results in a hang
|
||||
@@ -258,7 +261,7 @@ namespace ts.server {
|
||||
// buffer, but we have yet to find a way to retrieve that value.
|
||||
private static readonly maxActiveRequestCount = 10;
|
||||
private static readonly requestDelayMillis = 100;
|
||||
|
||||
private packageInstalledPromise: { resolve(value: ApplyCodeActionCommandResult): void, reject(reason: any): void };
|
||||
|
||||
constructor(
|
||||
private readonly telemetryEnabled: boolean,
|
||||
@@ -278,6 +281,31 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
isKnownTypesPackageName(name: string): boolean {
|
||||
// We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package.
|
||||
const validationResult = JsTyping.validatePackageName(name);
|
||||
if (validationResult !== JsTyping.PackageNameValidationResult.Ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.requestedRegistry) {
|
||||
return !!this.typesRegistryCache && this.typesRegistryCache.has(name);
|
||||
}
|
||||
|
||||
this.requestedRegistry = true;
|
||||
this.send({ kind: "typesRegistry" });
|
||||
return false;
|
||||
}
|
||||
|
||||
installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike<ApplyCodeActionCommandResult> {
|
||||
const rq: InstallPackageRequest = { kind: "installPackage", ...options };
|
||||
this.send(rq);
|
||||
Debug.assert(this.packageInstalledPromise === undefined);
|
||||
return new Promise((resolve, reject) => {
|
||||
this.packageInstalledPromise = { resolve, reject };
|
||||
});
|
||||
}
|
||||
|
||||
private reportInstallerProcessId() {
|
||||
if (this.installerPidReported) {
|
||||
return;
|
||||
@@ -343,7 +371,11 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
onProjectClosed(p: Project): void {
|
||||
this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" });
|
||||
this.send({ projectName: p.getProjectName(), kind: "closeProject" });
|
||||
}
|
||||
|
||||
private send(rq: TypingInstallerRequestUnion): void {
|
||||
this.installer.send(rq);
|
||||
}
|
||||
|
||||
enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void {
|
||||
@@ -359,7 +391,7 @@ namespace ts.server {
|
||||
if (this.logger.hasLevel(LogLevel.verbose)) {
|
||||
this.logger.info(`Sending request:${stringifyIndented(request)}`);
|
||||
}
|
||||
this.installer.send(request);
|
||||
this.send(request);
|
||||
};
|
||||
const queuedRequest: QueuedOperation = { operationId, operation };
|
||||
|
||||
@@ -375,12 +407,26 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) {
|
||||
private handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) {
|
||||
if (this.logger.hasLevel(LogLevel.verbose)) {
|
||||
this.logger.info(`Received response:${stringifyIndented(response)}`);
|
||||
}
|
||||
|
||||
switch (response.kind) {
|
||||
case EventTypesRegistry:
|
||||
this.typesRegistryCache = ts.createMapFromTemplate(response.typesRegistry);
|
||||
break;
|
||||
case EventPackageInstalled: {
|
||||
const { success, message } = response;
|
||||
if (success) {
|
||||
this.packageInstalledPromise.resolve({ successMessage: message });
|
||||
}
|
||||
else {
|
||||
this.packageInstalledPromise.reject(message);
|
||||
}
|
||||
this.packageInstalledPromise = undefined;
|
||||
break;
|
||||
}
|
||||
case EventInitializationFailed:
|
||||
{
|
||||
if (!this.eventSender) {
|
||||
|
||||
@@ -411,19 +411,27 @@ namespace ts.server {
|
||||
this.send(ev);
|
||||
}
|
||||
|
||||
public output(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) {
|
||||
// For backwards-compatibility only.
|
||||
public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void {
|
||||
this.doOutput(info, cmdName, reqSeq, /*success*/ !errorMsg, errorMsg);
|
||||
}
|
||||
|
||||
private doOutput(info: {} | undefined, cmdName: string, reqSeq: number, success: boolean, message?: string): void {
|
||||
const res: protocol.Response = {
|
||||
seq: 0,
|
||||
type: "response",
|
||||
command: cmdName,
|
||||
request_seq: reqSeq,
|
||||
success: !errorMsg,
|
||||
success,
|
||||
};
|
||||
if (!errorMsg) {
|
||||
if (success) {
|
||||
res.body = info;
|
||||
}
|
||||
else {
|
||||
res.message = errorMsg;
|
||||
Debug.assert(info === undefined);
|
||||
}
|
||||
if (message) {
|
||||
res.message = message;
|
||||
}
|
||||
this.send(res);
|
||||
}
|
||||
@@ -1307,7 +1315,7 @@ namespace ts.server {
|
||||
this.changeSeq++;
|
||||
// make sure no changes happen before this one is finished
|
||||
if (project.reloadScript(file, tempFileName)) {
|
||||
this.output(undefined, CommandNames.Reload, reqSeq);
|
||||
this.doOutput(/*info*/ undefined, CommandNames.Reload, reqSeq, /*success*/ true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1545,6 +1553,15 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
private applyCodeActionCommand(commandName: string, requestSeq: number, args: protocol.ApplyCodeActionCommandRequestArgs): void {
|
||||
const { file, project } = this.getFileAndProject(args);
|
||||
const output = (success: boolean, message: string) => this.doOutput({}, commandName, requestSeq, success, message);
|
||||
const command = args.command as CodeActionCommand; // They should be sending back the command we sent them.
|
||||
project.getLanguageService().applyCodeActionCommand(file, command).then(
|
||||
({ successMessage }) => { output(/*success*/ true, successMessage); },
|
||||
error => { output(/*success*/ false, error); });
|
||||
}
|
||||
|
||||
private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) {
|
||||
let startPosition: number = undefined, endPosition: number = undefined;
|
||||
if (args.startPosition !== undefined) {
|
||||
@@ -1567,14 +1584,12 @@ namespace ts.server {
|
||||
return { startPosition, endPosition };
|
||||
}
|
||||
|
||||
private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction {
|
||||
return {
|
||||
description: codeAction.description,
|
||||
changes: codeAction.changes.map(change => ({
|
||||
fileName: change.fileName,
|
||||
textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo))
|
||||
}))
|
||||
};
|
||||
private mapCodeAction({ description, changes: unmappedChanges, commands }: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction {
|
||||
const changes = unmappedChanges.map(change => ({
|
||||
fileName: change.fileName,
|
||||
textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo))
|
||||
}));
|
||||
return { description, changes, commands };
|
||||
}
|
||||
|
||||
private mapTextChangesToCodeEdits(project: Project, textChanges: FileTextChanges): protocol.FileCodeEdits {
|
||||
@@ -1660,15 +1675,15 @@ namespace ts.server {
|
||||
exit() {
|
||||
}
|
||||
|
||||
private notRequired() {
|
||||
private notRequired(): HandlerResponse {
|
||||
return { responseRequired: false };
|
||||
}
|
||||
|
||||
private requiredResponse(response: any) {
|
||||
private requiredResponse(response: {}): HandlerResponse {
|
||||
return { response, responseRequired: true };
|
||||
}
|
||||
|
||||
private handlers = createMapFromTemplate<(request: protocol.Request) => { response?: any, responseRequired?: boolean }>({
|
||||
private handlers = createMapFromTemplate<(request: protocol.Request) => HandlerResponse>({
|
||||
[CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => {
|
||||
this.projectService.openExternalProject(request.arguments, /*suppressRefreshOfInferredProjects*/ false);
|
||||
// TODO: report errors
|
||||
@@ -1846,7 +1861,7 @@ namespace ts.server {
|
||||
},
|
||||
[CommandNames.Configure]: (request: protocol.ConfigureRequest) => {
|
||||
this.projectService.setHostConfiguration(request.arguments);
|
||||
this.output(undefined, CommandNames.Configure, request.seq);
|
||||
this.doOutput(/*info*/ undefined, CommandNames.Configure, request.seq, /*success*/ true);
|
||||
return this.notRequired();
|
||||
},
|
||||
[CommandNames.Reload]: (request: protocol.ReloadRequest) => {
|
||||
@@ -1913,6 +1928,10 @@ namespace ts.server {
|
||||
[CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => {
|
||||
return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false));
|
||||
},
|
||||
[CommandNames.ApplyCodeActionCommand]: (request: protocol.ApplyCodeActionCommandRequest) => {
|
||||
this.applyCodeActionCommand(request.command, request.seq, request.arguments);
|
||||
return this.notRequired(); // Response will come asynchronously.
|
||||
},
|
||||
[CommandNames.GetSupportedCodeFixes]: () => {
|
||||
return this.requiredResponse(this.getSupportedCodeFixes());
|
||||
},
|
||||
@@ -1927,7 +1946,7 @@ namespace ts.server {
|
||||
}
|
||||
});
|
||||
|
||||
public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) {
|
||||
public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) {
|
||||
if (this.handlers.has(command)) {
|
||||
throw new Error(`Protocol handler already exists for command "${command}"`);
|
||||
}
|
||||
@@ -1956,14 +1975,14 @@ namespace ts.server {
|
||||
}
|
||||
}
|
||||
|
||||
public executeCommand(request: protocol.Request): { response?: any, responseRequired?: boolean } {
|
||||
public executeCommand(request: protocol.Request): HandlerResponse {
|
||||
const handler = this.handlers.get(request.command);
|
||||
if (handler) {
|
||||
return this.executeWithRequestId(request.seq, () => handler(request));
|
||||
}
|
||||
else {
|
||||
this.logger.msg(`Unrecognized JSON command:${stringifyIndented(request)}`, Msg.Err);
|
||||
this.output(undefined, CommandNames.Unknown, request.seq, `Unrecognized JSON command: ${request.command}`);
|
||||
this.doOutput(/*info*/ undefined, CommandNames.Unknown, request.seq, /*success*/ false, `Unrecognized JSON command: ${request.command}`);
|
||||
return { responseRequired: false };
|
||||
}
|
||||
}
|
||||
@@ -1994,25 +2013,31 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
if (response) {
|
||||
this.output(response, request.command, request.seq);
|
||||
this.doOutput(response, request.command, request.seq, /*success*/ true);
|
||||
}
|
||||
else if (responseRequired) {
|
||||
this.output(undefined, request.command, request.seq, "No content available.");
|
||||
this.doOutput(/*info*/ undefined, request.command, request.seq, /*success*/ false, "No content available.");
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof OperationCanceledException) {
|
||||
// Handle cancellation exceptions
|
||||
this.output({ canceled: true }, request.command, request.seq);
|
||||
this.doOutput({ canceled: true }, request.command, request.seq, /*success*/ true);
|
||||
return;
|
||||
}
|
||||
this.logError(err, message);
|
||||
this.output(
|
||||
undefined,
|
||||
this.doOutput(
|
||||
/*info*/ undefined,
|
||||
request ? request.command : CommandNames.Unknown,
|
||||
request ? request.seq : 0,
|
||||
/*success*/ false,
|
||||
"Error processing request. " + (<StackTraceError>err).message + "\n" + (<StackTraceError>err).stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface HandlerResponse {
|
||||
response?: {};
|
||||
responseRequired?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace ts.server {
|
||||
export const ActionSet: ActionSet = "action::set";
|
||||
export const ActionInvalidate: ActionInvalidate = "action::invalidate";
|
||||
export const EventTypesRegistry: EventTypesRegistry = "event::typesRegistry";
|
||||
export const EventPackageInstalled: EventPackageInstalled = "event::packageInstalled";
|
||||
export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes";
|
||||
export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes";
|
||||
export const EventInitializationFailed: EventInitializationFailed = "event::initializationFailed";
|
||||
|
||||
@@ -28,12 +28,14 @@ declare namespace ts.server {
|
||||
" __sortedArrayBrand": any;
|
||||
}
|
||||
|
||||
export interface TypingInstallerRequest {
|
||||
export interface TypingInstallerRequestWithProjectName {
|
||||
readonly projectName: string;
|
||||
readonly kind: "discover" | "closeProject";
|
||||
}
|
||||
|
||||
export interface DiscoverTypings extends TypingInstallerRequest {
|
||||
/* @internal */
|
||||
export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest;
|
||||
|
||||
export interface DiscoverTypings extends TypingInstallerRequestWithProjectName {
|
||||
readonly fileNames: string[];
|
||||
readonly projectRootPath: Path;
|
||||
readonly compilerOptions: CompilerOptions;
|
||||
@@ -43,18 +45,46 @@ declare namespace ts.server {
|
||||
readonly kind: "discover";
|
||||
}
|
||||
|
||||
export interface CloseProject extends TypingInstallerRequest {
|
||||
export interface CloseProject extends TypingInstallerRequestWithProjectName {
|
||||
readonly kind: "closeProject";
|
||||
}
|
||||
|
||||
export interface TypesRegistryRequest {
|
||||
readonly kind: "typesRegistry";
|
||||
}
|
||||
|
||||
export interface InstallPackageRequest {
|
||||
readonly kind: "installPackage";
|
||||
readonly fileName: Path;
|
||||
readonly packageName: string;
|
||||
readonly projectRootPath: Path;
|
||||
}
|
||||
|
||||
export type ActionSet = "action::set";
|
||||
export type ActionInvalidate = "action::invalidate";
|
||||
export type EventTypesRegistry = "event::typesRegistry";
|
||||
export type EventPackageInstalled = "event::packageInstalled";
|
||||
export type EventBeginInstallTypes = "event::beginInstallTypes";
|
||||
export type EventEndInstallTypes = "event::endInstallTypes";
|
||||
export type EventInitializationFailed = "event::initializationFailed";
|
||||
|
||||
export interface TypingInstallerResponse {
|
||||
readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
|
||||
readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | EventPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed;
|
||||
}
|
||||
/* @internal */
|
||||
export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse;
|
||||
|
||||
/* @internal */
|
||||
export interface TypesRegistryResponse extends TypingInstallerResponse {
|
||||
readonly kind: EventTypesRegistry;
|
||||
readonly typesRegistry: MapLike<void>;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export interface PackageInstalledResponse extends TypingInstallerResponse {
|
||||
readonly kind: EventPackageInstalled;
|
||||
readonly success: boolean;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export interface InitializationFailedResponse extends TypingInstallerResponse {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/// <reference path="project.ts"/>
|
||||
|
||||
namespace ts.server {
|
||||
export interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions {
|
||||
projectRootPath: Path;
|
||||
}
|
||||
|
||||
export interface ITypingsInstaller {
|
||||
isKnownTypesPackageName(name: string): boolean;
|
||||
installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike<ApplyCodeActionCommandResult>;
|
||||
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void;
|
||||
attach(projectService: ProjectService): void;
|
||||
onProjectClosed(p: Project): void;
|
||||
@@ -9,6 +15,9 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
export const nullTypingsInstaller: ITypingsInstaller = {
|
||||
isKnownTypesPackageName: returnFalse,
|
||||
// Should never be called because we never provide a types registry.
|
||||
installPackage: notImplemented,
|
||||
enqueueInstallTypingsRequest: noop,
|
||||
attach: noop,
|
||||
onProjectClosed: noop,
|
||||
@@ -77,6 +86,14 @@ namespace ts.server {
|
||||
constructor(private readonly installer: ITypingsInstaller) {
|
||||
}
|
||||
|
||||
isKnownTypesPackageName(name: string): boolean {
|
||||
return this.installer.isKnownTypesPackageName(name);
|
||||
}
|
||||
|
||||
installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike<ApplyCodeActionCommandResult> {
|
||||
return this.installer.installPackage(options);
|
||||
}
|
||||
|
||||
getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray<string>, forceRefresh: boolean): SortedReadonlyArray<string> {
|
||||
const typeAcquisition = project.getTypeAcquisition();
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace ts.server.typingsInstaller {
|
||||
}
|
||||
try {
|
||||
const content = <TypesRegistryFile>JSON.parse(host.readFile(typesRegistryFilePath));
|
||||
return createMapFromTemplate<void>(content.entries);
|
||||
return createMapFromTemplate(content.entries);
|
||||
}
|
||||
catch (e) {
|
||||
if (log.isEnabled()) {
|
||||
@@ -79,7 +79,7 @@ namespace ts.server.typingsInstaller {
|
||||
private readonly npmPath: string;
|
||||
readonly typesRegistry: Map<void>;
|
||||
|
||||
private delayedInitializationError: InitializationFailedResponse;
|
||||
private delayedInitializationError: InitializationFailedResponse | undefined;
|
||||
|
||||
constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, throttleLimit: number, log: Log) {
|
||||
super(
|
||||
@@ -127,7 +127,7 @@ namespace ts.server.typingsInstaller {
|
||||
}
|
||||
|
||||
listen() {
|
||||
process.on("message", (req: DiscoverTypings | CloseProject) => {
|
||||
process.on("message", (req: TypingInstallerRequestUnion) => {
|
||||
if (this.delayedInitializationError) {
|
||||
// report initializationFailed error
|
||||
this.sendResponse(this.delayedInitializationError);
|
||||
@@ -139,11 +139,39 @@ namespace ts.server.typingsInstaller {
|
||||
break;
|
||||
case "closeProject":
|
||||
this.closeProject(req);
|
||||
break;
|
||||
case "typesRegistry": {
|
||||
const typesRegistry: { [key: string]: void } = {};
|
||||
this.typesRegistry.forEach((value, key) => {
|
||||
typesRegistry[key] = value;
|
||||
});
|
||||
const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry };
|
||||
this.sendResponse(response);
|
||||
break;
|
||||
}
|
||||
case "installPackage": {
|
||||
const { fileName, packageName, projectRootPath } = req;
|
||||
const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath;
|
||||
if (cwd) {
|
||||
this.installWorker(-1, [packageName], cwd, success => {
|
||||
const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`;
|
||||
const response: PackageInstalledResponse = { kind: EventPackageInstalled, success, message };
|
||||
this.sendResponse(response);
|
||||
});
|
||||
}
|
||||
else {
|
||||
const response: PackageInstalledResponse = { kind: EventPackageInstalled, success: false, message: "Could not determine a project root path." };
|
||||
this.sendResponse(response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Debug.assertNever(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) {
|
||||
protected sendResponse(response: TypingInstallerResponseUnion) {
|
||||
if (this.log.isEnabled()) {
|
||||
this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`);
|
||||
}
|
||||
@@ -153,11 +181,11 @@ namespace ts.server.typingsInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
protected installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
||||
protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
||||
if (this.log.isEnabled()) {
|
||||
this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(args)}'.`);
|
||||
this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`);
|
||||
}
|
||||
const command = `${this.npmPath} install --ignore-scripts ${args.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`;
|
||||
const command = `${this.npmPath} install --ignore-scripts ${packageNames.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`;
|
||||
const start = Date.now();
|
||||
const hasError = this.execSyncAndLog(command, { cwd });
|
||||
if (this.log.isEnabled()) {
|
||||
@@ -186,6 +214,14 @@ namespace ts.server.typingsInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined {
|
||||
return forEachAncestorDirectory(getDirectoryPath(fileName), directory => {
|
||||
if (host.fileExists(combinePaths(directory, "package.json"))) {
|
||||
return directory;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const logFilePath = findArgument(server.Arguments.LogFile);
|
||||
const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation);
|
||||
const typingSafeListLocation = findArgument(server.Arguments.TypingSafeListLocation);
|
||||
|
||||
@@ -32,50 +32,11 @@ namespace ts.server.typingsInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
export enum PackageNameValidationResult {
|
||||
Ok,
|
||||
ScopedPackagesNotSupported,
|
||||
EmptyName,
|
||||
NameTooLong,
|
||||
NameStartsWithDot,
|
||||
NameStartsWithUnderscore,
|
||||
NameContainsNonURISafeCharacters
|
||||
}
|
||||
|
||||
|
||||
export 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 type RequestCompletedAction = (success: boolean) => void;
|
||||
interface PendingRequest {
|
||||
requestId: number;
|
||||
args: string[];
|
||||
packageNames: string[];
|
||||
cwd: string;
|
||||
onRequestCompleted: RequestCompletedAction;
|
||||
}
|
||||
@@ -255,8 +216,8 @@ namespace ts.server.typingsInstaller {
|
||||
if (this.missingTypingsSet.get(typing) || this.packageNameToTypingLocation.get(typing)) {
|
||||
continue;
|
||||
}
|
||||
const validationResult = validatePackageName(typing);
|
||||
if (validationResult === PackageNameValidationResult.Ok) {
|
||||
const validationResult = JsTyping.validatePackageName(typing);
|
||||
if (validationResult === JsTyping.PackageNameValidationResult.Ok) {
|
||||
if (this.typesRegistry.has(typing)) {
|
||||
result.push(typing);
|
||||
}
|
||||
@@ -270,26 +231,7 @@ namespace ts.server.typingsInstaller {
|
||||
// add typing name to missing set so we won't process it again
|
||||
this.missingTypingsSet.set(typing, true);
|
||||
if (this.log.isEnabled()) {
|
||||
switch (validationResult) {
|
||||
case PackageNameValidationResult.EmptyName:
|
||||
this.log.writeLine(`Package name '${typing}' cannot be empty`);
|
||||
break;
|
||||
case PackageNameValidationResult.NameTooLong:
|
||||
this.log.writeLine(`Package name '${typing}' should be less than ${MaxPackageNameLength} characters`);
|
||||
break;
|
||||
case PackageNameValidationResult.NameStartsWithDot:
|
||||
this.log.writeLine(`Package name '${typing}' cannot start with '.'`);
|
||||
break;
|
||||
case PackageNameValidationResult.NameStartsWithUnderscore:
|
||||
this.log.writeLine(`Package name '${typing}' cannot start with '_'`);
|
||||
break;
|
||||
case PackageNameValidationResult.ScopedPackagesNotSupported:
|
||||
this.log.writeLine(`Package '${typing}' is scoped and currently is not supported`);
|
||||
break;
|
||||
case PackageNameValidationResult.NameContainsNonURISafeCharacters:
|
||||
this.log.writeLine(`Package name '${typing}' contains non URI safe characters`);
|
||||
break;
|
||||
}
|
||||
this.log.writeLine(JsTyping.renderPackageNameValidationFailure(validationResult, typing));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -430,8 +372,8 @@ namespace ts.server.typingsInstaller {
|
||||
};
|
||||
}
|
||||
|
||||
private installTypingsAsync(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
||||
this.pendingRunRequests.unshift({ requestId, args, cwd, onRequestCompleted });
|
||||
private installTypingsAsync(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void {
|
||||
this.pendingRunRequests.unshift({ requestId, packageNames, cwd, onRequestCompleted });
|
||||
this.executeWithThrottling();
|
||||
}
|
||||
|
||||
@@ -439,7 +381,7 @@ namespace ts.server.typingsInstaller {
|
||||
while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) {
|
||||
this.inFlightRequestCount++;
|
||||
const request = this.pendingRunRequests.pop();
|
||||
this.installWorker(request.requestId, request.args, request.cwd, ok => {
|
||||
this.installWorker(request.requestId, request.packageNames, request.cwd, ok => {
|
||||
this.inFlightRequestCount--;
|
||||
request.onRequestCompleted(ok);
|
||||
this.executeWithThrottling();
|
||||
@@ -447,7 +389,7 @@ namespace ts.server.typingsInstaller {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
|
||||
protected abstract installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void;
|
||||
protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user