/
base.ts
196 lines (171 loc) · 5.52 KB
/
base.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
import { DendronError, getStage, isTSError } from "@dendronhq/common-all";
import { DLogger, getDurationMilliseconds } from "@dendronhq/common-server";
import _ from "lodash";
import { window } from "vscode";
import { IDendronExtension } from "../dendronExtensionInterface";
import { Logger } from "../logger";
import { IBaseCommand } from "../types";
import { AnalyticsUtils } from "../utils/analytics";
export type CodeCommandConstructor = {
new (extension: IDendronExtension): CodeCommandInstance;
requireActiveWorkspace: boolean;
};
export type CodeCommandInstance = {
key: string;
run: (opts?: any) => Promise<void>;
};
export type AnalyticProps = {
props?: any;
};
/** Anything other than `undefined` is an error and will stop the command. "cancel" will stop the command without displaying an error. */
export type SanityCheckResults = undefined | string | "cancel";
/**
* Base class for all Dendron Plugin Commands.
*
*
* Generics:
* - TOpts: passed into {@link BaseCommand.execute}
* - TOut: returned by {@link BaseCommand.execute}
* - TGatherOutput: returned by {@link BaseCommand.gatherInputs}
* - TRunOpts: passed into {@link BaseCommand.run}
*/
// eslint-disable-next-line no-redeclare
export abstract class BaseCommand<
TOpts,
TOut = any,
TGatherOutput = TOpts,
TRunOpts = TOpts
> implements IBaseCommand<TOpts, TOut, TGatherOutput, TRunOpts>
{
public L: DLogger;
constructor(_name?: string) {
this.L = Logger;
}
addAnalyticsPayload?(opts?: TOpts, out?: TOut): any;
static showInput = window.showInputBox;
/**
* Does this command require an active workspace in order to function
*/
static requireActiveWorkspace = false;
abstract key: string;
skipAnalytics?: boolean;
async gatherInputs(_opts?: TRunOpts): Promise<TGatherOutput | undefined> {
return {} as any;
}
abstract enrichInputs(inputs: TGatherOutput): Promise<TOpts | undefined>;
abstract execute(opts?: TOpts): Promise<TOut>;
async showResponse(_resp: TOut) {
return;
}
/** Check for errors and stop execution if needed, runs before `gatherInputs`. */
async sanityCheck(_opts?: Partial<TRunOpts>): Promise<SanityCheckResults> {
return;
}
protected mergeInputs(opts: TOpts, args?: Partial<TRunOpts>): TOpts {
return { ...opts, ...args };
}
async run(args?: Partial<TRunOpts>): Promise<TOut | undefined> {
const ctx = `${this.key}:run`;
const start = process.hrtime();
let isError = false;
let opts: TOpts | undefined;
let resp: TOut | undefined;
let sanityCheckResp: SanityCheckResults;
try {
sanityCheckResp = await this.sanityCheck(args);
if (sanityCheckResp === "cancel") {
this.L.info({ ctx, msg: "sanity check cancelled" });
return;
}
if (!_.isUndefined(sanityCheckResp) && sanityCheckResp !== "cancel") {
window.showErrorMessage(sanityCheckResp);
return;
}
// @ts-ignore
const inputs = await this.gatherInputs(args);
// if undefined, imply user cancel
if (_.isUndefined(inputs)) {
return;
}
opts = await this.enrichInputs(inputs);
if (_.isUndefined(opts)) {
return;
}
this.L.info({ ctx, msg: "pre-execute" });
resp = await this.execute(this.mergeInputs(opts, args));
this.L.info({ ctx, msg: "post-execute" });
this.showResponse(resp);
return resp;
} catch (error: any) {
let cerror: DendronError;
if (error instanceof DendronError) {
cerror = error;
} else if (isTSError(error)) {
cerror = new DendronError({
message: `error while running command: ${error.message}`,
innerError: error,
});
} else {
cerror = new DendronError({
message: `unknown error while running command`,
});
}
Logger.error({
ctx,
error: cerror,
});
isError = true;
// During development only, rethrow the errors to make them easier to debug
if (getStage() === "dev") {
throw error;
}
return;
} finally {
const payload = this.addAnalyticsPayload
? await this.addAnalyticsPayload(opts, resp)
: {};
const sanityCheckResults = sanityCheckResp
? { sanityCheck: sanityCheckResp }
: {};
if (!this.skipAnalytics)
AnalyticsUtils.track(this.key, {
duration: getDurationMilliseconds(start),
error: isError,
...payload,
...sanityCheckResults,
});
}
}
}
/**
* Command with no enriched inputs
*/
export abstract class BasicCommand<
TOpts,
TOut = any,
TRunOpts = TOpts
> extends BaseCommand<TOpts, TOut, TOpts, TRunOpts> {
async enrichInputs(inputs: TOpts): Promise<TOpts> {
return inputs;
}
}
/** This command passes the output of `gatherOpts`/`enrichOpts` directly to `execute`.
*
* The regular command class tries to merge the inputs from `gatherOpts` and `enrichOpts` together, which
* will break your code if you use any `TOpts` that is not a basic js object.
*
* This is especially useful for commands that accept input directly from VSCode, like {@link ShowPreviewCommand}
*/
export abstract class InputArgCommand<TOpts, TOut = any> extends BasicCommand<
TOpts,
TOut,
TOpts
> {
async gatherInputs(opts?: TOpts): Promise<TOpts | undefined> {
// The cast and return is needed because if `opts` is `undefined` then `run` will just skip doing `execute`
return opts || ({} as TOpts);
}
protected mergeInputs(opts: TOpts, _args?: Partial<TOpts>): TOpts {
return opts;
}
}