1 | import execa from 'execa';
|
2 | import mergeWith from 'lodash/mergeWith';
|
3 | import { PrimitiveType } from '@boost/args';
|
4 | import { Blueprint, isObject, optimal, Path, Predicates, predicates, toArray } from '@boost/common';
|
5 | import { ConcurrentEvent, Event } from '@boost/event';
|
6 | import { Plugin } from '@boost/plugin';
|
7 | import {
|
8 | STRATEGY_BUFFER,
|
9 | STRATEGY_COPY,
|
10 | STRATEGY_CREATE,
|
11 | STRATEGY_NATIVE,
|
12 | STRATEGY_NONE,
|
13 | STRATEGY_PIPE,
|
14 | STRATEGY_REFERENCE,
|
15 | STRATEGY_STREAM,
|
16 | STRATEGY_TEMPLATE,
|
17 | } from './constants';
|
18 | import { ConfigContext } from './contexts/ConfigContext';
|
19 | import { DriverContext } from './contexts/DriverContext';
|
20 | import { isClassInstance } from './helpers/isClassInstance';
|
21 | import {
|
22 | Argv,
|
23 | BeemoTool,
|
24 | ConfigObject,
|
25 | Driverable,
|
26 | DriverCommandConfig,
|
27 | DriverCommandRegistration,
|
28 | DriverCommandRunner,
|
29 | DriverConfigStrategy,
|
30 | DriverMetadata,
|
31 | DriverOptions,
|
32 | DriverOutput,
|
33 | DriverOutputStrategy,
|
34 | Execution,
|
35 | } from './types';
|
36 |
|
37 | export abstract class Driver<
|
38 | Config extends object = {},
|
39 | Options extends DriverOptions = DriverOptions,
|
40 | >
|
41 | extends Plugin<BeemoTool, Options>
|
42 | implements Driverable
|
43 | {
|
44 |
|
45 | commands: DriverCommandRegistration<any, any>[] = [];
|
46 |
|
47 |
|
48 | config!: Config;
|
49 |
|
50 |
|
51 | metadata!: DriverMetadata;
|
52 |
|
53 |
|
54 | tool!: BeemoTool;
|
55 |
|
56 | output: DriverOutput = {
|
57 | stderr: '',
|
58 | stdout: '',
|
59 | };
|
60 |
|
61 | readonly onLoadProviderConfig = new Event<[ConfigContext, Path, Config]>('load-provider-config');
|
62 |
|
63 | readonly onLoadConsumerConfig = new Event<[ConfigContext, Config]>('load-consumer-config');
|
64 |
|
65 | readonly onMergeConfig = new Event<[ConfigContext, Config]>('merge-config');
|
66 |
|
67 | readonly onCreateConfigFile = new Event<[ConfigContext, Path, Config]>('create-config-file');
|
68 |
|
69 | readonly onCopyConfigFile = new Event<[ConfigContext, Path, Config]>('copy-config-file');
|
70 |
|
71 | readonly onReferenceConfigFile = new Event<[ConfigContext, Path, Config]>(
|
72 | 'reference-config-file',
|
73 | );
|
74 |
|
75 | readonly onTemplateConfigFile = new Event<[ConfigContext, Path, ConfigObject | string]>(
|
76 | 'template-config-file',
|
77 | );
|
78 |
|
79 | readonly onDeleteConfigFile = new Event<[ConfigContext, Path]>('delete-config-file');
|
80 |
|
81 | readonly onBeforeExecute = new ConcurrentEvent<[DriverContext, Argv]>('before-execute');
|
82 |
|
83 | readonly onAfterExecute = new ConcurrentEvent<[DriverContext, unknown]>('after-execute');
|
84 |
|
85 | readonly onFailedExecute = new ConcurrentEvent<[DriverContext, Error]>('failed-execute');
|
86 |
|
87 | static validate(driver: Driver) {
|
88 | const name = (isClassInstance(driver) && driver.constructor.name) || 'Driver';
|
89 |
|
90 | if (!isObject(driver.options)) {
|
91 | throw new Error(`\`${name}\` requires an options object.`);
|
92 | }
|
93 | }
|
94 |
|
95 | blueprint({ array, object, string, bool }: Predicates): Blueprint<DriverOptions> {
|
96 | return {
|
97 | args: array(string()),
|
98 | configStrategy: string(STRATEGY_NATIVE).oneOf<DriverConfigStrategy>([
|
99 | STRATEGY_NATIVE,
|
100 | STRATEGY_CREATE,
|
101 | STRATEGY_REFERENCE,
|
102 | STRATEGY_TEMPLATE,
|
103 | STRATEGY_COPY,
|
104 | STRATEGY_NONE,
|
105 | ]),
|
106 | dependencies: array(string()),
|
107 | env: object(string()),
|
108 | expandGlobs: bool(true),
|
109 | outputStrategy: string(STRATEGY_BUFFER).oneOf<DriverOutputStrategy>([
|
110 | STRATEGY_BUFFER,
|
111 | STRATEGY_PIPE,
|
112 | STRATEGY_STREAM,
|
113 | STRATEGY_NONE,
|
114 | ]),
|
115 | template: string(),
|
116 | };
|
117 | }
|
118 |
|
119 | bootstrap() {}
|
120 |
|
121 | override startup(tool: BeemoTool) {
|
122 | this.tool = tool;
|
123 | this.bootstrap();
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 | doMerge(prevValue: unknown, nextValue: unknown): unknown {
|
130 | if (Array.isArray(prevValue) && Array.isArray(nextValue)) {
|
131 |
|
132 | return [...new Set([...prevValue, ...nextValue])] as unknown;
|
133 | }
|
134 |
|
135 | return undefined;
|
136 | }
|
137 |
|
138 | |
139 |
|
140 |
|
141 | extractErrorMessage(error: { message: string }): string {
|
142 | return error.message.split('\n', 1)[0] || '';
|
143 | }
|
144 |
|
145 | |
146 |
|
147 |
|
148 | formatConfig(data: Config): string {
|
149 | const content = JSON.stringify(data, null, 2);
|
150 |
|
151 | if (this.metadata.configName.endsWith('.js')) {
|
152 | return `module.exports = ${content};`;
|
153 | }
|
154 |
|
155 | return content;
|
156 | }
|
157 |
|
158 | |
159 |
|
160 |
|
161 | getName(): string {
|
162 | return this.name.split('-').pop()!;
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 | getArgs(): Argv {
|
169 | return toArray(this.options.args);
|
170 | }
|
171 |
|
172 | |
173 |
|
174 |
|
175 | getDependencies(): string[] {
|
176 | return [
|
177 |
|
178 | ...this.metadata.dependencies,
|
179 |
|
180 | ...toArray(this.options.dependencies),
|
181 | ];
|
182 | }
|
183 |
|
184 | |
185 |
|
186 |
|
187 | getOutputStrategy(): DriverOutputStrategy {
|
188 | return (this.tool.config.execute.output || this.options.outputStrategy) ?? STRATEGY_BUFFER;
|
189 | }
|
190 |
|
191 | |
192 |
|
193 |
|
194 | getSupportedOptions(): string[] {
|
195 | return [];
|
196 | }
|
197 |
|
198 | |
199 |
|
200 |
|
201 | getVersion(): string {
|
202 | const { bin, versionOption } = this.metadata;
|
203 | const version = (execa.sync(bin, [versionOption], { preferLocal: true })?.stdout || '').trim();
|
204 | const match = version.match(/(\d+)\.(\d+)\.(\d+)/u);
|
205 |
|
206 | return match ? match[0] : '0.0.0';
|
207 | }
|
208 |
|
209 | |
210 |
|
211 |
|
212 | mergeConfig(prev: Config, next: Config): Config {
|
213 | return mergeWith(prev, next, this.doMerge);
|
214 | }
|
215 |
|
216 | |
217 |
|
218 |
|
219 | processFailure(error: Execution) {
|
220 | const { stderr = '', stdout = '' } = error;
|
221 | const out = (stderr || stdout).trim();
|
222 |
|
223 | if (out) {
|
224 | this.setOutput('stderr', out);
|
225 | }
|
226 | }
|
227 |
|
228 | |
229 |
|
230 |
|
231 | processSuccess(response: Execution) {
|
232 | const { stderr = '', stdout = '' } = response;
|
233 |
|
234 | this.setOutput('stderr', stderr.trim());
|
235 | this.setOutput('stdout', stdout.trim());
|
236 | }
|
237 |
|
238 | |
239 |
|
240 |
|
241 | registerCommand<O extends object, P extends PrimitiveType[]>(
|
242 | path: string,
|
243 | config: DriverCommandConfig<O, P>,
|
244 | runner: DriverCommandRunner<O, P>,
|
245 | ) {
|
246 | this.commands.push({ config, path, runner });
|
247 |
|
248 | return this;
|
249 | }
|
250 |
|
251 | |
252 |
|
253 |
|
254 | setMetadata(metadata: Partial<DriverMetadata>): this {
|
255 | const { array, bool, string, object, shape } = predicates;
|
256 |
|
257 | this.metadata = optimal(
|
258 | metadata,
|
259 | {
|
260 | bin: string()
|
261 | .match(/^[a-z]{1}[a-zA-Z0-9-]+$/u)
|
262 | .required(),
|
263 | commandOptions: object(
|
264 | shape({
|
265 | description: string().required(),
|
266 | type: string().oneOf<'boolean' | 'number' | 'string'>(['string', 'number', 'boolean']),
|
267 | }),
|
268 | ),
|
269 | configName: string().required(),
|
270 | configOption: string('--config'),
|
271 | configStrategy: string(STRATEGY_CREATE).oneOf([
|
272 | STRATEGY_CREATE,
|
273 | STRATEGY_REFERENCE,
|
274 | STRATEGY_COPY,
|
275 | ]),
|
276 | dependencies: array(string()),
|
277 | description: string(),
|
278 | filterOptions: bool(true),
|
279 | helpOption: string('--help'),
|
280 | title: string().required(),
|
281 | useConfigOption: bool(),
|
282 | versionOption: string('--version'),
|
283 | watchOptions: array(string()),
|
284 | workspaceStrategy: string(STRATEGY_REFERENCE).oneOf([STRATEGY_REFERENCE, STRATEGY_COPY]),
|
285 | },
|
286 | {
|
287 | name: this.constructor.name,
|
288 | },
|
289 | );
|
290 |
|
291 | return this;
|
292 | }
|
293 |
|
294 | |
295 |
|
296 |
|
297 | setOutput(type: keyof DriverOutput, value: string): this {
|
298 | this.output[type] = value.trim();
|
299 |
|
300 | return this;
|
301 | }
|
302 | }
|