1 | "use strict";
|
---|
2 | /**
|
---|
3 | * @license
|
---|
4 | * Copyright Google LLC All Rights Reserved.
|
---|
5 | *
|
---|
6 | * Use of this source code is governed by an MIT-style license that can be
|
---|
7 | * found in the LICENSE file at https://angular.io/license
|
---|
8 | */
|
---|
9 | Object.defineProperty(exports, "__esModule", { value: true });
|
---|
10 | exports.BuilderHarness = void 0;
|
---|
11 | const architect_1 = require("@angular-devkit/architect");
|
---|
12 | const core_1 = require("@angular-devkit/core");
|
---|
13 | const rxjs_1 = require("rxjs");
|
---|
14 | const operators_1 = require("rxjs/operators");
|
---|
15 | const file_watching_1 = require("./file-watching");
|
---|
16 | class BuilderHarness {
|
---|
17 | constructor(builderHandler, host, builderInfo) {
|
---|
18 | this.builderHandler = builderHandler;
|
---|
19 | this.host = host;
|
---|
20 | this.schemaRegistry = new core_1.json.schema.CoreSchemaRegistry();
|
---|
21 | this.projectName = 'test';
|
---|
22 | this.projectMetadata = { root: '.', sourceRoot: 'src' };
|
---|
23 | this.options = new Map();
|
---|
24 | this.builderTargets = new Map();
|
---|
25 | // Generate default pseudo builder info for test purposes
|
---|
26 | this.builderInfo = {
|
---|
27 | builderName: builderHandler.name,
|
---|
28 | description: '',
|
---|
29 | optionSchema: true,
|
---|
30 | ...builderInfo,
|
---|
31 | };
|
---|
32 | this.schemaRegistry.addPostTransform(core_1.json.schema.transforms.addUndefinedDefaults);
|
---|
33 | }
|
---|
34 | useProject(name, metadata = {}) {
|
---|
35 | if (!name) {
|
---|
36 | throw new Error('Project name cannot be an empty string.');
|
---|
37 | }
|
---|
38 | this.projectName = name;
|
---|
39 | this.projectMetadata = metadata;
|
---|
40 | return this;
|
---|
41 | }
|
---|
42 | useTarget(name, baseOptions) {
|
---|
43 | if (!name) {
|
---|
44 | throw new Error('Target name cannot be an empty string.');
|
---|
45 | }
|
---|
46 | this.targetName = name;
|
---|
47 | this.options.set(null, baseOptions);
|
---|
48 | return this;
|
---|
49 | }
|
---|
50 | withConfiguration(configuration, options) {
|
---|
51 | this.options.set(configuration, options);
|
---|
52 | return this;
|
---|
53 | }
|
---|
54 | withBuilderTarget(target, handler, options, info) {
|
---|
55 | this.builderTargets.set(target, {
|
---|
56 | handler,
|
---|
57 | options: options || {},
|
---|
58 | info: { builderName: handler.name, description: '', optionSchema: true, ...info },
|
---|
59 | });
|
---|
60 | return this;
|
---|
61 | }
|
---|
62 | execute(options = {}) {
|
---|
63 | var _a;
|
---|
64 | const { configuration, outputLogsOnException = true, outputLogsOnFailure = true, useNativeFileWatching = false, } = options;
|
---|
65 | const targetOptions = {
|
---|
66 | ...this.options.get(null),
|
---|
67 | ...((_a = (configuration && this.options.get(configuration))) !== null && _a !== void 0 ? _a : {}),
|
---|
68 | };
|
---|
69 | if (!useNativeFileWatching) {
|
---|
70 | if (this.watcherNotifier) {
|
---|
71 | throw new Error('Only one harness execution at a time is supported.');
|
---|
72 | }
|
---|
73 | this.watcherNotifier = new file_watching_1.WatcherNotifier();
|
---|
74 | }
|
---|
75 | const contextHost = {
|
---|
76 | findBuilderByTarget: async (project, target) => {
|
---|
77 | this.validateProjectName(project);
|
---|
78 | if (target === this.targetName) {
|
---|
79 | return {
|
---|
80 | info: this.builderInfo,
|
---|
81 | handler: this.builderHandler,
|
---|
82 | };
|
---|
83 | }
|
---|
84 | const builderTarget = this.builderTargets.get(target);
|
---|
85 | if (builderTarget) {
|
---|
86 | return { info: builderTarget.info, handler: builderTarget.handler };
|
---|
87 | }
|
---|
88 | throw new Error('Project target does not exist.');
|
---|
89 | },
|
---|
90 | async getBuilderName(project, target) {
|
---|
91 | return (await this.findBuilderByTarget(project, target)).info.builderName;
|
---|
92 | },
|
---|
93 | getMetadata: async (project) => {
|
---|
94 | this.validateProjectName(project);
|
---|
95 | return this.projectMetadata;
|
---|
96 | },
|
---|
97 | getOptions: async (project, target, configuration) => {
|
---|
98 | var _a, _b;
|
---|
99 | this.validateProjectName(project);
|
---|
100 | if (target === this.targetName) {
|
---|
101 | return (_a = this.options.get(configuration !== null && configuration !== void 0 ? configuration : null)) !== null && _a !== void 0 ? _a : {};
|
---|
102 | }
|
---|
103 | else if (configuration !== undefined) {
|
---|
104 | // Harness builder targets currently do not support configurations
|
---|
105 | return {};
|
---|
106 | }
|
---|
107 | else {
|
---|
108 | return ((_b = this.builderTargets.get(target)) === null || _b === void 0 ? void 0 : _b.options) || {};
|
---|
109 | }
|
---|
110 | },
|
---|
111 | hasTarget: async (project, target) => {
|
---|
112 | this.validateProjectName(project);
|
---|
113 | return this.targetName === target || this.builderTargets.has(target);
|
---|
114 | },
|
---|
115 | getDefaultConfigurationName: async (_project, _target) => {
|
---|
116 | return undefined;
|
---|
117 | },
|
---|
118 | validate: async (options, builderName) => {
|
---|
119 | let schema;
|
---|
120 | if (builderName === this.builderInfo.builderName) {
|
---|
121 | schema = this.builderInfo.optionSchema;
|
---|
122 | }
|
---|
123 | else {
|
---|
124 | for (const [, value] of this.builderTargets) {
|
---|
125 | if (value.info.builderName === builderName) {
|
---|
126 | schema = value.info.optionSchema;
|
---|
127 | break;
|
---|
128 | }
|
---|
129 | }
|
---|
130 | }
|
---|
131 | const validator = await this.schemaRegistry.compile(schema !== null && schema !== void 0 ? schema : true).toPromise();
|
---|
132 | const { data } = await validator(options).toPromise();
|
---|
133 | return data;
|
---|
134 | },
|
---|
135 | };
|
---|
136 | const context = new HarnessBuilderContext(this.builderInfo, core_1.getSystemPath(this.host.root()), contextHost, useNativeFileWatching ? undefined : this.watcherNotifier);
|
---|
137 | if (this.targetName !== undefined) {
|
---|
138 | context.target = {
|
---|
139 | project: this.projectName,
|
---|
140 | target: this.targetName,
|
---|
141 | configuration: configuration,
|
---|
142 | };
|
---|
143 | }
|
---|
144 | const logs = [];
|
---|
145 | context.logger.subscribe((e) => logs.push(e));
|
---|
146 | return this.schemaRegistry.compile(this.builderInfo.optionSchema).pipe(operators_1.mergeMap((validator) => validator(targetOptions)), operators_1.map((validationResult) => validationResult.data), operators_1.mergeMap((data) => convertBuilderOutputToObservable(this.builderHandler(data, context))), operators_1.map((buildResult) => ({ result: buildResult, error: undefined })), operators_1.catchError((error) => {
|
---|
147 | if (outputLogsOnException) {
|
---|
148 | // eslint-disable-next-line no-console
|
---|
149 | console.error(logs.map((entry) => entry.message).join('\n'));
|
---|
150 | // eslint-disable-next-line no-console
|
---|
151 | console.error(error);
|
---|
152 | }
|
---|
153 | return rxjs_1.of({ result: undefined, error });
|
---|
154 | }), operators_1.map(({ result, error }) => {
|
---|
155 | if (outputLogsOnFailure && (result === null || result === void 0 ? void 0 : result.success) === false && logs.length > 0) {
|
---|
156 | // eslint-disable-next-line no-console
|
---|
157 | console.error(logs.map((entry) => entry.message).join('\n'));
|
---|
158 | }
|
---|
159 | // Capture current logs and clear for next
|
---|
160 | const currentLogs = logs.slice();
|
---|
161 | logs.length = 0;
|
---|
162 | return { result, error, logs: currentLogs };
|
---|
163 | }), operators_1.finalize(() => {
|
---|
164 | this.watcherNotifier = undefined;
|
---|
165 | for (const teardown of context.teardowns) {
|
---|
166 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
|
---|
167 | teardown();
|
---|
168 | }
|
---|
169 | }));
|
---|
170 | }
|
---|
171 | async executeOnce(options) {
|
---|
172 | // Return the first result
|
---|
173 | return this.execute(options).pipe(operators_1.first()).toPromise();
|
---|
174 | }
|
---|
175 | async appendToFile(path, content) {
|
---|
176 | await this.writeFile(path, this.readFile(path).concat(content));
|
---|
177 | }
|
---|
178 | async writeFile(path, content) {
|
---|
179 | var _a;
|
---|
180 | this.host
|
---|
181 | .scopedSync()
|
---|
182 | .write(core_1.normalize(path), typeof content === 'string' ? Buffer.from(content) : content);
|
---|
183 | (_a = this.watcherNotifier) === null || _a === void 0 ? void 0 : _a.notify([
|
---|
184 | { path: core_1.getSystemPath(core_1.join(this.host.root(), path)), type: 'modified' },
|
---|
185 | ]);
|
---|
186 | }
|
---|
187 | async writeFiles(files) {
|
---|
188 | var _a;
|
---|
189 | const watchEvents = this.watcherNotifier
|
---|
190 | ? []
|
---|
191 | : undefined;
|
---|
192 | for (const [path, content] of Object.entries(files)) {
|
---|
193 | this.host
|
---|
194 | .scopedSync()
|
---|
195 | .write(core_1.normalize(path), typeof content === 'string' ? Buffer.from(content) : content);
|
---|
196 | watchEvents === null || watchEvents === void 0 ? void 0 : watchEvents.push({ path: core_1.getSystemPath(core_1.join(this.host.root(), path)), type: 'modified' });
|
---|
197 | }
|
---|
198 | if (watchEvents) {
|
---|
199 | (_a = this.watcherNotifier) === null || _a === void 0 ? void 0 : _a.notify(watchEvents);
|
---|
200 | }
|
---|
201 | }
|
---|
202 | async removeFile(path) {
|
---|
203 | var _a;
|
---|
204 | this.host.scopedSync().delete(core_1.normalize(path));
|
---|
205 | (_a = this.watcherNotifier) === null || _a === void 0 ? void 0 : _a.notify([
|
---|
206 | { path: core_1.getSystemPath(core_1.join(this.host.root(), path)), type: 'deleted' },
|
---|
207 | ]);
|
---|
208 | }
|
---|
209 | async modifyFile(path, modifier) {
|
---|
210 | var _a;
|
---|
211 | const content = this.readFile(path);
|
---|
212 | await this.writeFile(path, await modifier(content));
|
---|
213 | (_a = this.watcherNotifier) === null || _a === void 0 ? void 0 : _a.notify([
|
---|
214 | { path: core_1.getSystemPath(core_1.join(this.host.root(), path)), type: 'modified' },
|
---|
215 | ]);
|
---|
216 | }
|
---|
217 | hasFile(path) {
|
---|
218 | return this.host.scopedSync().exists(core_1.normalize(path));
|
---|
219 | }
|
---|
220 | hasFileMatch(directory, pattern) {
|
---|
221 | return this.host
|
---|
222 | .scopedSync()
|
---|
223 | .list(core_1.normalize(directory))
|
---|
224 | .some((name) => pattern.test(name));
|
---|
225 | }
|
---|
226 | readFile(path) {
|
---|
227 | const content = this.host.scopedSync().read(core_1.normalize(path));
|
---|
228 | return Buffer.from(content).toString('utf8');
|
---|
229 | }
|
---|
230 | validateProjectName(name) {
|
---|
231 | if (name !== this.projectName) {
|
---|
232 | throw new Error(`Project "${name}" does not exist.`);
|
---|
233 | }
|
---|
234 | }
|
---|
235 | }
|
---|
236 | exports.BuilderHarness = BuilderHarness;
|
---|
237 | class HarnessBuilderContext {
|
---|
238 | constructor(builder, basePath, contextHost, watcherFactory) {
|
---|
239 | this.builder = builder;
|
---|
240 | this.contextHost = contextHost;
|
---|
241 | this.watcherFactory = watcherFactory;
|
---|
242 | this.id = Math.trunc(Math.random() * 1000000);
|
---|
243 | this.logger = new core_1.logging.Logger(`builder-harness-${this.id}`);
|
---|
244 | this.teardowns = [];
|
---|
245 | this.workspaceRoot = this.currentDirectory = basePath;
|
---|
246 | }
|
---|
247 | get analytics() {
|
---|
248 | // Can be undefined even though interface does not allow it
|
---|
249 | return undefined;
|
---|
250 | }
|
---|
251 | addTeardown(teardown) {
|
---|
252 | this.teardowns.push(teardown);
|
---|
253 | }
|
---|
254 | async getBuilderNameForTarget(target) {
|
---|
255 | return this.contextHost.getBuilderName(target.project, target.target);
|
---|
256 | }
|
---|
257 | async getProjectMetadata(targetOrName) {
|
---|
258 | const project = typeof targetOrName === 'string' ? targetOrName : targetOrName.project;
|
---|
259 | return this.contextHost.getMetadata(project);
|
---|
260 | }
|
---|
261 | async getTargetOptions(target) {
|
---|
262 | return this.contextHost.getOptions(target.project, target.target, target.configuration);
|
---|
263 | }
|
---|
264 | // Unused by builders in this package
|
---|
265 | async scheduleBuilder(builderName, options, scheduleOptions) {
|
---|
266 | throw new Error('Not Implemented.');
|
---|
267 | }
|
---|
268 | async scheduleTarget(target, overrides, scheduleOptions) {
|
---|
269 | const { info, handler } = await this.contextHost.findBuilderByTarget(target.project, target.target);
|
---|
270 | const targetOptions = await this.validateOptions({
|
---|
271 | ...(await this.getTargetOptions(target)),
|
---|
272 | ...overrides,
|
---|
273 | }, info.builderName);
|
---|
274 | const context = new HarnessBuilderContext(info, this.workspaceRoot, this.contextHost, this.watcherFactory);
|
---|
275 | context.target = target;
|
---|
276 | context.logger = (scheduleOptions === null || scheduleOptions === void 0 ? void 0 : scheduleOptions.logger) || this.logger.createChild('');
|
---|
277 | const progressSubject = new rxjs_1.Subject();
|
---|
278 | const output = convertBuilderOutputToObservable(handler(targetOptions, context));
|
---|
279 | const run = {
|
---|
280 | id: context.id,
|
---|
281 | info,
|
---|
282 | progress: progressSubject.asObservable(),
|
---|
283 | async stop() {
|
---|
284 | for (const teardown of context.teardowns) {
|
---|
285 | await teardown();
|
---|
286 | }
|
---|
287 | progressSubject.complete();
|
---|
288 | },
|
---|
289 | output: output.pipe(operators_1.shareReplay()),
|
---|
290 | get result() {
|
---|
291 | return this.output.pipe(operators_1.first()).toPromise();
|
---|
292 | },
|
---|
293 | };
|
---|
294 | return run;
|
---|
295 | }
|
---|
296 | async validateOptions(options, builderName) {
|
---|
297 | return this.contextHost.validate(options, builderName);
|
---|
298 | }
|
---|
299 | // Unused report methods
|
---|
300 | reportRunning() { }
|
---|
301 | reportStatus() { }
|
---|
302 | reportProgress() { }
|
---|
303 | }
|
---|
304 | function isAsyncIterable(obj) {
|
---|
305 | return !!obj && typeof obj[Symbol.asyncIterator] === 'function';
|
---|
306 | }
|
---|
307 | function convertBuilderOutputToObservable(output) {
|
---|
308 | if (architect_1.isBuilderOutput(output)) {
|
---|
309 | return rxjs_1.of(output);
|
---|
310 | }
|
---|
311 | else if (isAsyncIterable(output)) {
|
---|
312 | return architect_1.fromAsyncIterable(output);
|
---|
313 | }
|
---|
314 | else {
|
---|
315 | return rxjs_1.from(output);
|
---|
316 | }
|
---|
317 | }
|
---|