From b773190fec3c74981a49f63e70c8a57fbe48e3a2 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 10:56:21 -0800 Subject: [PATCH 01/11] refactor: remove _/builders private package It was only used when manually testing the old Architect. --- packages/_/builders/builders.json | 10 ---------- packages/_/builders/package.json | 12 ------------ packages/_/builders/src/noop-schema.json | 4 ---- packages/_/builders/src/true.ts | 20 -------------------- 4 files changed, 46 deletions(-) delete mode 100644 packages/_/builders/builders.json delete mode 100644 packages/_/builders/package.json delete mode 100644 packages/_/builders/src/noop-schema.json delete mode 100644 packages/_/builders/src/true.ts diff --git a/packages/_/builders/builders.json b/packages/_/builders/builders.json deleted file mode 100644 index 87914ed792d2..000000000000 --- a/packages/_/builders/builders.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "../architect/src/builders-schema.json", - "builders": { - "true": { - "class": "./src/true", - "schema": "./src/noop-schema.json", - "description": "Always succeed." - } - } -} diff --git a/packages/_/builders/package.json b/packages/_/builders/package.json deleted file mode 100644 index 0f9d3729d481..000000000000 --- a/packages/_/builders/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@_/builders", - "version": "0.0.0", - "description": "CLI tool for Angular", - "main": "src/index.js", - "typings": "src/index.d.ts", - "builders": "builders.json", - "private": true, - "dependencies": { - "rxjs": "6.3.3" - } -} diff --git a/packages/_/builders/src/noop-schema.json b/packages/_/builders/src/noop-schema.json deleted file mode 100644 index afadf0925f37..000000000000 --- a/packages/_/builders/src/noop-schema.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "type": "object" -} \ No newline at end of file diff --git a/packages/_/builders/src/true.ts b/packages/_/builders/src/true.ts deleted file mode 100644 index efe9b7cb0046..000000000000 --- a/packages/_/builders/src/true.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { Observable, of } from 'rxjs'; - -export class TrueBuilder { - constructor() {} - - run(): Observable<{ success: boolean }> { - return of({ - success: true, - }); - } -} - -export default TrueBuilder; From b81ec783475283af1692d8399e3033270d205006 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 10:57:51 -0800 Subject: [PATCH 02/11] feat(@angular-devkit/core): export terminal capabilities --- packages/angular_devkit/core/src/terminal/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular_devkit/core/src/terminal/index.ts b/packages/angular_devkit/core/src/terminal/index.ts index a33a3ba65ac9..d853f3c1c9bb 100644 --- a/packages/angular_devkit/core/src/terminal/index.ts +++ b/packages/angular_devkit/core/src/terminal/index.ts @@ -6,4 +6,5 @@ * found in the LICENSE file at https://angular.io/license */ export * from './text'; +export * from './caps'; export * from './colors'; From b057839fc2d46db0fdadb410b2670e82873e8c6c Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:03:59 -0800 Subject: [PATCH 03/11] feat(@angular-devkit/core): logger.log() should keep own metadata It was possible to overwrite the metadata of the logger itself (name, path) when calling "log()". This should not happen. If there is a need to overwrite the loggers metadata itself one should use "next()" and construct or forward their own log entry. --- packages/angular_devkit/core/src/logger/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular_devkit/core/src/logger/logger.ts b/packages/angular_devkit/core/src/logger/logger.ts index f4682a80d41e..08bb66a117cd 100644 --- a/packages/angular_devkit/core/src/logger/logger.ts +++ b/packages/angular_devkit/core/src/logger/logger.ts @@ -102,7 +102,7 @@ export class Logger extends Observable implements LoggerApi { } log(level: LogLevel, message: string, metadata: JsonObject = {}): void { - const entry: LogEntry = Object.assign({}, this._metadata, metadata, { + const entry: LogEntry = Object.assign({}, metadata, this._metadata, { level, message, timestamp: +Date.now(), }); this._subject.next(entry); From 1d8fad691ebd4291377f40ea3ffdd9d09035361c Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:15:58 -0800 Subject: [PATCH 04/11] feat(@angular-devkit/core): jobs should re-log instead of forwarding Current behaviour is to have logs forwarded, but this is flawed because on the job side the logger is actually re-created. This allows logs to be actually part of the caller side logging infrastructure. --- .../core/src/experimental/jobs/create-job-handler.ts | 2 +- .../core/src/experimental/jobs/simple-scheduler.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts b/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts index 5e62439a078b..40b63c94bd05 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts +++ b/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts @@ -101,7 +101,7 @@ export function createJobHandler { subject.next({ kind: JobOutboundMessageKind.Log, diff --git a/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts b/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts index c7edcd907613..76ade7049bb1 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts +++ b/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts @@ -305,7 +305,7 @@ export class SimpleScheduler< let state = JobState.Queued; let pingId = 0; - const logger = options.logger ? options.logger.createChild('job') : new NullLogger(); + const logger = options.logger ? options.logger : new NullLogger(); // Create the input channel by having a filter. const input = new Subject(); @@ -349,7 +349,8 @@ export class SimpleScheduler< switch (message.kind) { case JobOutboundMessageKind.Log: - logger.next(message.entry); + const entry = message.entry; + logger.log(entry.level, entry.message, entry); break; case JobOutboundMessageKind.ChannelCreate: { From a8a5c8f526ff45ccc5e5a967f020cccd0d3b5e51 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:30:45 -0800 Subject: [PATCH 05/11] feat(@angular-devkit/architect): New Architect API first draft The new API has been described in this design doc: https://docs.google.com/document/d/1SpN_2XEooI9_CPjqspAcNEBjVY874OWPZqOenjuF0qo/view This first drafts add support for the API (given some deep imports). It is still in draft mode but is committed to make it available to people to start testing and moving their own builders. This API rebuilds (not backward compatible) the Architect API package. To use it people will need to import "@angular-devkit/architect/src/index2" to start using it. A reference builder will be added in the next commit. There are 2 pieces missing from this commit that will be added in the same PR; 1) the architect-host and CLI to test, and 2) a reference builder moved from the old API to the new one. These will be part of the same PR. Finally, there are missing tests in this package, but everything tested manually and automatically works so far. Test coverage will be added before the package is considered finished. Due to a desire to keep architect, our tests and the scope of this PR limited and keep the two APIs separated, every clashing files will have a "2" suffix added to it. Once all builders have been moved and we are sure everything works, all those files will be moved to their final destination and the old API will be removed, in one PR. --- .../src/{index.d.ts => _golden-api.d.ts} | 0 packages/angular_devkit/architect/BUILD | 21 ++ .../architect/src/_golden-api.d.ts | 11 + packages/angular_devkit/architect/src/api.ts | 230 ++++++++++++++ .../angular_devkit/architect/src/architect.ts | 288 ++++++++++++++++++ .../architect/src/builders-schema.json | 63 ++-- .../architect/src/create-builder.ts | 171 +++++++++++ .../angular_devkit/architect/src/index.ts | 5 + .../angular_devkit/architect/src/index2.ts | 10 + .../architect/src/index2_spec.ts | 138 +++++++++ .../architect/src/input-schema.json | 47 +++ .../angular_devkit/architect/src/internal.ts | 81 +++++ .../architect/src/output-schema.json | 34 +++ .../architect/src/progress-schema.json | 102 +++++++ .../architect/src/schedule-by-name.ts | 121 ++++++++ .../architect/testing/index2.ts | 9 + .../testing/testing-architect-host.ts | 108 +++++++ 17 files changed, 1422 insertions(+), 17 deletions(-) rename etc/api/angular_devkit/architect/src/{index.d.ts => _golden-api.d.ts} (100%) create mode 100644 packages/angular_devkit/architect/src/_golden-api.d.ts create mode 100644 packages/angular_devkit/architect/src/api.ts create mode 100644 packages/angular_devkit/architect/src/architect.ts create mode 100644 packages/angular_devkit/architect/src/create-builder.ts create mode 100644 packages/angular_devkit/architect/src/index2.ts create mode 100644 packages/angular_devkit/architect/src/index2_spec.ts create mode 100644 packages/angular_devkit/architect/src/input-schema.json create mode 100644 packages/angular_devkit/architect/src/internal.ts create mode 100644 packages/angular_devkit/architect/src/output-schema.json create mode 100644 packages/angular_devkit/architect/src/progress-schema.json create mode 100644 packages/angular_devkit/architect/src/schedule-by-name.ts create mode 100644 packages/angular_devkit/architect/testing/index2.ts create mode 100644 packages/angular_devkit/architect/testing/testing-architect-host.ts diff --git a/etc/api/angular_devkit/architect/src/index.d.ts b/etc/api/angular_devkit/architect/src/_golden-api.d.ts similarity index 100% rename from etc/api/angular_devkit/architect/src/index.d.ts rename to etc/api/angular_devkit/architect/src/_golden-api.d.ts diff --git a/packages/angular_devkit/architect/BUILD b/packages/angular_devkit/architect/BUILD index 49dc46ed8807..98e2432d239d 100644 --- a/packages/angular_devkit/architect/BUILD +++ b/packages/angular_devkit/architect/BUILD @@ -7,9 +7,27 @@ licenses(["notice"]) # MIT load("@build_bazel_rules_typescript//:defs.bzl", "ts_library") load("@build_bazel_rules_nodejs//:defs.bzl", "npm_package") +load("//tools:ts_json_schema.bzl", "ts_json_schema") package(default_visibility = ["//visibility:public"]) +ts_json_schema( + name = "builder_input_schema", + src = "src/input-schema.json", +) +ts_json_schema( + name = "builder_output_schema", + src = "src/output-schema.json", +) +ts_json_schema( + name = "builder_builders_schema", + src = "src/builders-schema.json", +) +ts_json_schema( + name = "progress_schema", + src = "src/progress-schema.json", +) + ts_library( name = "architect", srcs = glob( @@ -29,6 +47,9 @@ ts_library( "@rxjs", "@rxjs//operators", "@npm//@types/node", + ":builder_input_schema", + ":builder_output_schema", + ":progress_schema", ], ) diff --git a/packages/angular_devkit/architect/src/_golden-api.d.ts b/packages/angular_devkit/architect/src/_golden-api.d.ts new file mode 100644 index 000000000000..3e50b7278e3e --- /dev/null +++ b/packages/angular_devkit/architect/src/_golden-api.d.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './architect-legacy'; +// export * from './api'; +// export { Architect, ScheduleOptions } from './architect'; +// export { createBuilder } from './create-builder'; diff --git a/packages/angular_devkit/architect/src/api.ts b/packages/angular_devkit/architect/src/api.ts new file mode 100644 index 000000000000..6aabb6160448 --- /dev/null +++ b/packages/angular_devkit/architect/src/api.ts @@ -0,0 +1,230 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental, json, logging } from '@angular-devkit/core'; +import { Observable } from 'rxjs'; +import { Schema as RealBuilderInput, Target as RealTarget } from './input-schema'; +import { Schema as RealBuilderOutput } from './output-schema'; +import { Schema as RealBuilderProgress, State as BuilderProgressState } from './progress-schema'; + +export type Target = json.JsonObject & RealTarget; +export { + BuilderProgressState, +}; + +// Type short hands. +export type BuilderRegistry = + experimental.jobs.Registry; + + +/** + * An API typed BuilderProgress. The interface generated from the schema is too permissive, + * so this API is the one we show in our API. Please note that not all fields are in there; this + * is in addition to fields in the schema. + */ +export type TypedBuilderProgress = ( + { state: BuilderProgressState.Stopped; } + | { state: BuilderProgressState.Error; error: json.JsonValue; } + | { state: BuilderProgressState.Waiting; status?: string; } + | { state: BuilderProgressState.Running; status?: string; current: number; total?: number; } +); + +/** + * Declaration of those types as JsonObject compatible. JsonObject is not compatible with + * optional members, so those wouldn't be directly assignable to our internal Json typings. + * Forcing the type to be both a JsonObject and the type from the Schema tells Typescript they + * are compatible (which they are). + * These types should be used everywhere. + */ +export type BuilderInput = json.JsonObject & RealBuilderInput; +export type BuilderOutput = json.JsonObject & RealBuilderOutput; +export type BuilderProgress = json.JsonObject & RealBuilderProgress & TypedBuilderProgress; + +/** + * A progress report is what the tooling will receive. It contains the builder info and the target. + * Although these are serializable, they are only exposed through the tooling interface, not the + * builder interface. The watch dog sends BuilderProgress and the Builder has a set of functions + * to manage the state. + */ +export type BuilderProgressReport = BuilderProgress & ({ + target?: Target; + builder: BuilderInfo; +}); + +/** + * A Run, which is what is returned by scheduleBuilder or scheduleTarget functions. This should + * be reconstructed across memory boundaries (it's not serializable but all internal information + * are). + */ +export interface BuilderRun { + /** + * Unique amongst runs. This is the same ID as the context generated for the run. It can be + * used to identify multiple unique runs. There is no guarantee that a run is a single output; + * a builder can rebuild on its own and will generate multiple outputs. + */ + id: number; + + /** + * The builder information. + */ + info: BuilderInfo; + + /** + * The next output from a builder. This is recommended when scheduling a builder and only being + * interested in the result of that single run, not of a watch-mode builder. + */ + result: Promise; + + /** + * The output(s) from the builder. A builder can have multiple outputs. + * This always replay the last output when subscribed. + */ + output: Observable; + + /** + * The progress report. A progress also contains an ID, which can be different than this run's + * ID (if the builder calls scheduleBuilder or scheduleTarget). + * This will always replay the last progress on new subscriptions. + */ + progress: Observable; + + /** + * Stop the builder from running. Returns a promise that resolves when the builder is stopped. + * Some builders might not handle stopping properly and should have a timeout here. + */ + stop(): Promise; +} + +/** + * The context received as a second argument in your builder. + */ +export interface BuilderContext { + /** + * Unique amongst contexts. Contexts instances are not guaranteed to be the same (but it could + * be the same context), and all the fields in a context could be the same, yet the builder's + * context could be different. This is the same ID as the corresponding run. + */ + id: number; + + /** + * The builder info that called your function. Since the builder info is from the builder.json + * (or the host), it could contain information that is different than expected. + */ + builder: BuilderInfo; + + /** + * A logger that appends messages to a log. This could be a separate interface or completely + * ignored. `console.log` could also be completely ignored. + */ + logger: logging.LoggerApi; + + /** + * The absolute workspace root of this run. This is a system path and will not be normalized; + * ie. on Windows it will starts with `C:\\` (or whatever drive). + */ + workspaceRoot: string; + + /** + * The current directory the user is in. This could be outside the workspace root. This is a + * system path and will not be normalized; ie. on Windows it will starts with `C:\\` (or + * whatever drive). + */ + currentDirectory: string; + + /** + * The target that was used to run this builder. + * Target is optional if a builder was ran using `scheduleBuilder()`. + */ + target?: Target; + + /** + * Schedule a target in the same workspace. This can be the same target that is being executed + * right now, but targets of the same name are serialized. + * Running the same target and waiting for it to end will result in a deadlocking scenario. + * Targets are considered the same if the project, the target AND the configuration are the same. + * @param target The target to schedule. + * @param overrides A set of options to override the workspace set of options. + * @return A promise of a run. It will resolve when all the members of the run are available. + */ + scheduleTarget( + target: Target, + overrides?: json.JsonObject, + ): Promise; + + /** + * Schedule a builder by its name. This can be the same builder that is being executed. + * @param builderName The name of the builder, ie. its `packageName:builderName` tuple. + * @param options All options to use for the builder (by default empty object). There is no + * additional options added, e.g. from the workspace. + * @return A promise of a run. It will resolve when all the members of the run are available. + */ + scheduleBuilder( + builderName: string, + options?: json.JsonObject, + ): Promise; + + /** + * Set the builder to running. This should be used if an external event triggered a re-run, + * e.g. a file watched was changed. + */ + reportRunning(): void; + + /** + * Update the status string shown on the interface. + * @param status The status to set it to. An empty string can be used to remove the status. + */ + reportStatus(status: string): void; + + /** + * Update the progress for this builder run. + * @param current The current progress. This will be between 0 and total. + * @param total A new total to set. By default at the start of a run this is 1. If omitted it + * will use the same value as the last total. + * @param status Update the status string. If omitted the status string is not modified. + */ + reportProgress(current: number, total?: number, status?: string): void; +} + + +/** + * An accepted return value from a builder. Can be either an Observable, a Promise or a vector. + */ +export type BuilderOutputLike = Observable | Promise | BuilderOutput; + + +/** + * A builder handler function. The function signature passed to `createBuilder()`. + */ +export interface BuilderHandlerFn { + /** + * Builders are defined by users to perform any kind of task, like building, testing or linting, + * and should use this interface. + * @param input The options (a JsonObject), validated by the schema and received by the + * builder. This can include resolved options from the CLI or the workspace. + * @param context A context that can be used to interact with the Architect framework. + * @return One or many builder output. + */ + (input: A, context: BuilderContext): BuilderOutputLike; +} + +/** + * A Builder general information. This is generated by the host and is expanded by the host, but + * the public API contains those fields. + */ +export type BuilderInfo = json.JsonObject & { + builderName: string; + description: string; + optionSchema: json.schema.JsonSchema; +}; + + +/** + * Returns a string of "project:target[:configuration]" for the target object. + */ +export function targetStringFromTarget({project, target, configuration}: Target) { + return `${project}:${target}${configuration !== undefined ? ':' + configuration : ''}`; +} diff --git a/packages/angular_devkit/architect/src/architect.ts b/packages/angular_devkit/architect/src/architect.ts new file mode 100644 index 000000000000..36f46a7eca1e --- /dev/null +++ b/packages/angular_devkit/architect/src/architect.ts @@ -0,0 +1,288 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental, json, logging } from '@angular-devkit/core'; +import { Observable, from, of } from 'rxjs'; +import { concatMap, first, map, shareReplay } from 'rxjs/operators'; +import { + BuilderInfo, + BuilderInput, + BuilderOutput, + BuilderRegistry, + BuilderRun, + Target, + targetStringFromTarget, +} from './api'; +import { ArchitectHost, BuilderDescription, BuilderJobHandler } from './internal'; +import { scheduleByName, scheduleByTarget } from './schedule-by-name'; + +const inputSchema = require('./input-schema.json'); +const outputSchema = require('./output-schema.json'); + +function _createJobHandlerFromBuilderInfo( + info: BuilderInfo, + target: Target | undefined, + host: ArchitectHost, + registry: json.schema.SchemaRegistry, + baseOptions: json.JsonObject, +): Observable { + const jobDescription: BuilderDescription = { + name: target ? `{${targetStringFromTarget(target)}}` : info.builderName, + argument: { type: 'object' }, + input: inputSchema, + output: outputSchema, + info, + }; + + function handler(argument: json.JsonObject, context: experimental.jobs.JobHandlerContext) { + const inboundBus = context.inboundBus.pipe( + concatMap(message => { + if (message.kind === experimental.jobs.JobInboundMessageKind.Input) { + const v = message.value as BuilderInput; + const options = { + ...baseOptions, + ...v.options, + }; + + // Validate v against the options schema. + return registry.compile(info.optionSchema).pipe( + concatMap(validation => validation(options)), + map(result => { + if (result.success) { + return { ...v, options: result.data } as BuilderInput; + } else if (result.errors) { + throw new Error('Options did not validate.' + result.errors.join()); + } else { + return v; + } + }), + map(value => ({ ...message, value })), + ); + } else { + return of(message as experimental.jobs.JobInboundMessage); + } + }), + // Using a share replay because the job might be synchronously sending input, but + // asynchronously listening to it. + shareReplay(1), + ); + + return from(host.loadBuilder(info)).pipe( + concatMap(builder => { + if (builder === null) { + throw new Error(`Cannot load builder for builderInfo ${JSON.stringify(info, null, 2)}`); + } + + return builder.handler(argument, { ...context, inboundBus }).pipe( + map(output => { + if (output.kind === experimental.jobs.JobOutboundMessageKind.Output) { + // Add target to it. + return { + ...output, + value: { + ...output.value, + ...target ? { target } : 0, + } as json.JsonObject, + }; + } else { + return output; + } + }), + ); + }), + ); + } + + return of(Object.assign(handler, { jobDescription }) as BuilderJobHandler); +} + +export interface ScheduleOptions { + logger?: logging.Logger; +} + + +/** + * A JobRegistry that resolves builder targets from the host. + */ +export class ArchitectBuilderJobRegistry implements BuilderRegistry { + constructor( + protected _host: ArchitectHost, + protected _registry: json.schema.SchemaRegistry, + protected _jobCache?: Map>, + protected _infoCache?: Map>, + ) {} + + protected _resolveBuilder(name: string): Observable { + const cache = this._infoCache; + if (cache) { + const maybeCache = cache.get(name); + if (maybeCache !== undefined) { + return maybeCache; + } + + const info = from(this._host.resolveBuilder(name)).pipe( + shareReplay(1), + ); + cache.set(name, info); + + return info; + } + + return from(this._host.resolveBuilder(name)); + } + + protected _createBuilder( + info: BuilderInfo, + target?: Target, + options?: json.JsonObject, + ): Observable { + const cache = this._jobCache; + if (target) { + const maybeHit = cache && cache.get(targetStringFromTarget(target)); + if (maybeHit) { + return maybeHit; + } + } else { + const maybeHit = cache && cache.get(info.builderName); + if (maybeHit) { + return maybeHit; + } + } + + const result = _createJobHandlerFromBuilderInfo( + info, + target, + this._host, + this._registry, + options || {}, + ); + + if (cache) { + if (target) { + cache.set(targetStringFromTarget(target), result.pipe(shareReplay(1))); + } else { + cache.set(info.builderName, result.pipe(shareReplay(1))); + } + } + + return result; + } + + get< + A extends json.JsonObject, + I extends BuilderInput, + O extends BuilderOutput, + >(name: string): Observable | null> { + const m = name.match(/^([^:]+):([^:]+)$/i); + if (!m) { + return of(null); + } + + return from(this._resolveBuilder(name)).pipe( + concatMap(builderInfo => builderInfo ? this._createBuilder(builderInfo) : of(null)), + first(null, null), + ) as Observable | null>; + } +} + +/** + * A JobRegistry that resolves targets from the host. + */ +export class ArchitectTargetJobRegistry extends ArchitectBuilderJobRegistry { + get< + A extends json.JsonObject, + I extends BuilderInput, + O extends BuilderOutput, + >(name: string): Observable | null> { + const m = name.match(/^{([^:]+):([^:]+)(?::([^:]*))?}$/i); + if (!m) { + return of(null); + } + + const target = { + project: m[1], + target: m[2], + configuration: m[3], + }; + + return from(Promise.all([ + this._host.getBuilderNameForTarget(target), + this._host.getOptionsForTarget(target), + ])).pipe( + concatMap(([builderStr, options]) => { + if (builderStr === null || options === null) { + return of(null); + } + + return this._resolveBuilder(builderStr).pipe( + concatMap(builderInfo => { + if (builderInfo === null) { + return of(null); + } + + return this._createBuilder(builderInfo, target, options); + }), + ); + }), + first(null, null), + ) as Observable | null>; + } +} + + +export class Architect { + private readonly _scheduler: experimental.jobs.Scheduler; + private readonly _jobCache = new Map>(); + private readonly _infoCache = new Map>(); + + constructor( + private _host: ArchitectHost, + private _registry: json.schema.SchemaRegistry = new json.schema.CoreSchemaRegistry(), + additionalJobRegistry?: experimental.jobs.Registry, + ) { + const jobRegistry = new experimental.jobs.FallbackRegistry([ + new ArchitectTargetJobRegistry(_host, _registry, this._jobCache, this._infoCache), + new ArchitectBuilderJobRegistry(_host, _registry, this._jobCache, this._infoCache), + ...(additionalJobRegistry ? [additionalJobRegistry] : []), + ] as experimental.jobs.Registry[]); + + this._scheduler = new experimental.jobs.SimpleScheduler(jobRegistry, _registry); + } + + has(name: experimental.jobs.JobName) { + return this._scheduler.has(name); + } + + scheduleBuilder( + name: string, + options: json.JsonObject, + scheduleOptions: ScheduleOptions = {}, + ): Promise { + if (!/^[^:]+:[^:]+$/.test(name)) { + throw new Error('Invalid builder name: ' + JSON.stringify(name)); + } + + return scheduleByName(name, options, { + scheduler: this._scheduler, + logger: scheduleOptions.logger || new logging.NullLogger(), + currentDirectory: this._host.getCurrentDirectory(), + workspaceRoot: this._host.getWorkspaceRoot(), + }); + } + scheduleTarget( + target: Target, + overrides: json.JsonObject = {}, + scheduleOptions: ScheduleOptions = {}, + ): Promise { + return scheduleByTarget(target, overrides, { + scheduler: this._scheduler, + logger: scheduleOptions.logger || new logging.NullLogger(), + currentDirectory: this._host.getCurrentDirectory(), + workspaceRoot: this._host.getWorkspaceRoot(), + }); + } +} diff --git a/packages/angular_devkit/architect/src/builders-schema.json b/packages/angular_devkit/architect/src/builders-schema.json index d111b24f0103..8c3f3e6cbc24 100644 --- a/packages/angular_devkit/architect/src/builders-schema.json +++ b/packages/angular_devkit/architect/src/builders-schema.json @@ -9,33 +9,62 @@ "description": "Link to schema." }, "builders": { + "type": "object", "additionalProperties": { "$ref": "#/definitions/builder" } } }, + "required": [ + "builders" + ], "definitions": { "builder": { "type": "object", - "description": "Target options.", - "properties": { - "class": { - "type": "string", - "description": "The builder class module." - }, - "schema": { - "type": "string", - "description": "Schema for builder option validation." + "description": "Target options for Builders.", + "allOf": [ + { + "properties": { + "schema": { + "type": "string", + "description": "Schema for builder option validation." + }, + "description": { + "type": "string", + "description": "Builder description." + } + }, + "required": [ + "schema", + "description" + ] }, - "description": { - "type": "string", - "description": "Builder description." + { + "anyOf": [ + { + "properties": { + "implementation": { + "type": "string", + "description": "The next generation builder module." + } + }, + "required": [ + "implementation" + ] + }, + { + "properties": { + "class": { + "type": "string", + "description": "The builder class module." + } + }, + "required": [ + "class" + ] + } + ] } - }, - "required": [ - "class", - "schema", - "description" ] } } diff --git a/packages/angular_devkit/architect/src/create-builder.ts b/packages/angular_devkit/architect/src/create-builder.ts new file mode 100644 index 000000000000..56053ad81a27 --- /dev/null +++ b/packages/angular_devkit/architect/src/create-builder.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental, isPromise, json } from '@angular-devkit/core'; +import { Observable, Subscription, from, isObservable, of } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { + BuilderContext, + BuilderHandlerFn, + BuilderInfo, + BuilderInput, + BuilderOutput, + BuilderProgressState, + Target, + TypedBuilderProgress, +} from './api'; +import { Builder, BuilderSymbol, BuilderVersionSymbol } from './internal'; +import { scheduleByName, scheduleByTarget } from './schedule-by-name'; + + +export function createBuilder( + fn: BuilderHandlerFn, +): Builder { + const cjh = experimental.jobs.createJobHandler; + const handler = cjh((options, context) => { + const scheduler = context.scheduler; + const logger = context.logger; + const progressChannel = context.createChannel('progress'); + let currentState: BuilderProgressState = BuilderProgressState.Stopped; + let current = 0; + let status = ''; + let total = 1; + + function progress(progress: TypedBuilderProgress, context: BuilderContext) { + currentState = progress.state; + if (progress.state === BuilderProgressState.Running) { + current = progress.current; + total = progress.total !== undefined ? progress.total : total; + + if (progress.status === undefined) { + progress.status = status; + } else { + status = progress.status; + } + } + + progressChannel.next({ + ...progress as json.JsonObject, + ...(context.target && { target: context.target }), + ...(context.builder && { builder: context.builder }), + id: context.id, + }); + } + + return new Observable(observer => { + const subscriptions: Subscription[] = []; + + const inputSubscription = context.inboundBus.subscribe( + i => { + switch (i.kind) { + case experimental.jobs.JobInboundMessageKind.Stop: + observer.complete(); + break; + case experimental.jobs.JobInboundMessageKind.Input: + onInput(i.value); + break; + } + }, + ); + + function onInput(i: BuilderInput) { + const builder = i.info as BuilderInfo; + const context: BuilderContext = { + builder, + workspaceRoot: i.workspaceRoot, + currentDirectory: i.currentDirectory, + target: i.target as Target, + logger: logger, + id: i.id, + async scheduleTarget(target: Target, overrides: json.JsonObject = {}) { + const run = await scheduleByTarget(target, overrides, { + scheduler, + logger: logger.createChild(''), + workspaceRoot: i.workspaceRoot, + currentDirectory: i.currentDirectory, + }); + + // We don't want to subscribe errors and complete. + subscriptions.push(run.progress.subscribe(event => progressChannel.next(event))); + + return run; + }, + async scheduleBuilder(builderName: string, options: json.JsonObject = {}) { + const run = await scheduleByName(builderName, options, { + scheduler, + logger: logger.createChild(''), + workspaceRoot: i.workspaceRoot, + currentDirectory: i.currentDirectory, + }); + + // We don't want to subscribe errors and complete. + subscriptions.push(run.progress.subscribe(event => progressChannel.next(event))); + + return run; + }, + reportRunning() { + switch (currentState) { + case BuilderProgressState.Waiting: + case BuilderProgressState.Stopped: + progress({state: BuilderProgressState.Running, current: 0, total}, context); + break; + } + }, + reportStatus(status: string) { + switch (currentState) { + case BuilderProgressState.Running: + progress({ state: currentState, status, current, total }, context); + break; + case BuilderProgressState.Waiting: + progress({ state: currentState, status }, context); + break; + } + }, + reportProgress(current: number, total?: number, status?: string) { + switch (currentState) { + case BuilderProgressState.Running: + progress({ state: currentState, current, total, status }, context); + } + }, + }; + + context.reportRunning(); + let result = fn(i.options as OptT, context); + + if (isPromise(result)) { + result = from(result); + } else if (!isObservable(result)) { + result = of(result); + } + + // Manage some state automatically. + progress({ state: BuilderProgressState.Running, current: 0, total: 1 }, context); + subscriptions.push(result.pipe( + tap(() => { + progress({ state: BuilderProgressState.Running, current: total }, context); + progress({ state: BuilderProgressState.Stopped }, context); + }), + ).subscribe( + message => observer.next(message), + error => observer.error(error), + () => observer.complete(), + )); + } + + return () => { + subscriptions.forEach(x => x.unsubscribe()); + inputSubscription.unsubscribe(); + }; + }); + }); + + return { + handler, + [BuilderSymbol]: true, + [BuilderVersionSymbol]: require('../package.json').version, + }; +} diff --git a/packages/angular_devkit/architect/src/index.ts b/packages/angular_devkit/architect/src/index.ts index 1041aa4e5be0..d219e68bc09d 100644 --- a/packages/angular_devkit/architect/src/index.ts +++ b/packages/angular_devkit/architect/src/index.ts @@ -9,3 +9,8 @@ * @deprecated */ export * from './architect-legacy'; + +import * as index2 from './index2'; +export { + index2, +}; diff --git a/packages/angular_devkit/architect/src/index2.ts b/packages/angular_devkit/architect/src/index2.ts new file mode 100644 index 000000000000..a21d4ff324b2 --- /dev/null +++ b/packages/angular_devkit/architect/src/index2.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './api'; +export { Architect, ScheduleOptions } from './architect'; +export { createBuilder } from './create-builder'; diff --git a/packages/angular_devkit/architect/src/index2_spec.ts b/packages/angular_devkit/architect/src/index2_spec.ts new file mode 100644 index 000000000000..d8c7b876c844 --- /dev/null +++ b/packages/angular_devkit/architect/src/index2_spec.ts @@ -0,0 +1,138 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { schema } from '@angular-devkit/core'; +import { timer } from 'rxjs'; +import { map, take, tap, toArray } from 'rxjs/operators'; +import { TestingArchitectHost } from '../testing/testing-architect-host'; +import { BuilderOutput } from './api'; +import { Architect } from './architect'; +import { createBuilder } from './create-builder'; + +describe('architect', () => { + let testArchitectHost: TestingArchitectHost; + let architect: Architect; + let called = 0; + let options: {} = {}; + const target1 = { + project: 'test', + target: 'test', + }; + const target2 = { + project: 'test', + target: 'abc', + }; + + beforeEach(async () => { + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + testArchitectHost = new TestingArchitectHost(); + architect = new Architect(testArchitectHost, registry); + + called = 0; + testArchitectHost.addBuilder('package:test', createBuilder(async () => { + called++; + + return new Promise(resolve => { + setTimeout(() => resolve({ success: true }), 10); + }); + })); + testArchitectHost.addBuilder('package:test-options', createBuilder(o => { + options = o; + + return { success: true }; + })); + + testArchitectHost.addTarget(target1, 'package:test'); + testArchitectHost.addTarget(target2, 'package:test'); + }); + + it('works', async () => { + testArchitectHost.addBuilder('package:test', createBuilder(() => ({ success: true }))); + + const run = await architect.scheduleBuilder('package:test', {}); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + await run.stop(); + }); + + it('works with async builders', async () => { + testArchitectHost.addBuilder('package:test', createBuilder(async () => ({ success: true }))); + + const run = await architect.scheduleBuilder('package:test', {}); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + await run.stop(); + }); + + it('runs builders parallel', async () => { + const run = await architect.scheduleBuilder('package:test', {}); + const run2 = await architect.scheduleBuilder('package:test', {}); + expect(called).toBe(2); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + expect(await run2.result).toEqual(jasmine.objectContaining({ success: true })); + expect(called).toBe(2); + await run.stop(); + }); + + it('runs targets parallel', async () => { + const run = await architect.scheduleTarget(target1, {}); + const run2 = await architect.scheduleTarget(target1, {}); + expect(called).toBe(2); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + expect(await run2.result).toEqual(jasmine.objectContaining({ success: true })); + expect(called).toBe(2); + await run.stop(); + }); + + it('passes options to builders', async () => { + const o = { hello: 'world' }; + const run = await architect.scheduleBuilder('package:test-options', o); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + expect(options).toEqual(o); + }); + + it('passes options to targets', async () => { + const o = { hello: 'world' }; + const run = await architect.scheduleTarget(target1, o); + expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); + expect(options).toEqual(o); + }); + + it('errors when builder cannot be resolved', async () => { + try { + await architect.scheduleBuilder('non:existent', {}); + expect('to throw').not.toEqual('to throw'); + } catch { + } + }); + + it('works with watching builders', async () => { + let results = 0; + testArchitectHost.addBuilder('package:test-watch', createBuilder((_, context) => { + called++; + + return timer(10, 10).pipe( + take(10), + map(() => { + context.reportRunning(); + + return { success: true }; + }), + tap(() => results++), + ); + })); + + const run = await architect.scheduleBuilder('package:test-watch', {}); + await run.result; + expect(called).toBe(1); + expect(results).toBe(1); + + const all = await run.output.pipe(toArray()).toPromise(); + expect(called).toBe(1); + expect(results).toBe(10); + expect(all.length).toBe(10); + }); +}); diff --git a/packages/angular_devkit/architect/src/input-schema.json b/packages/angular_devkit/architect/src/input-schema.json new file mode 100644 index 000000000000..47d9c30014e5 --- /dev/null +++ b/packages/angular_devkit/architect/src/input-schema.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "BuilderInputSchema", + "title": "Input schema for builders.", + "type": "object", + "properties": { + "workspaceRoot": { + "type": "string" + }, + "currentDirectory": { + "type": "string" + }, + "id": { + "type": "number" + }, + "target": { + "type": "object", + "properties": { + "project": { + "type": "string" + }, + "target": { + "type": "string" + }, + "configuration": { + "type": "string" + } + }, + "required": [ + "project", + "target" + ] + }, + "info": { + "type": "object" + }, + "options": { + "type": "object" + } + }, + "required": [ + "currentDirectory", + "id", + "info", + "workspaceRoot" + ] +} diff --git a/packages/angular_devkit/architect/src/internal.ts b/packages/angular_devkit/architect/src/internal.ts new file mode 100644 index 000000000000..0ccf439926ee --- /dev/null +++ b/packages/angular_devkit/architect/src/internal.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental, json } from '@angular-devkit/core'; +import { BuilderInfo, BuilderInput, BuilderOutput, Target } from './api'; + +// Internal types that should not be exported directly. These are used by the host and architect +// itself. Host implementations should import the host.ts file. + +/** + * BuilderSymbol used for knowing if a function was created using createBuilder(). This is a + * property set on the function that should be `true`. + * Using Symbol.for() as it's a global registry that's the same for all installations of + * Architect (if some libraries depends directly on architect instead of sharing the files). + */ +export const BuilderSymbol = Symbol.for('@angular-devkit/architect:builder'); + +/** + * BuilderVersionSymbol used for knowing which version of the library createBuilder() came from. + * This is to make sure we don't try to use an incompatible builder. + * Using Symbol.for() as it's a global registry that's the same for all installations of + * Architect (if some libraries depends directly on architect instead of sharing the files). + */ +export const BuilderVersionSymbol = Symbol.for('@angular-devkit/architect:version'); + +/** + * A Specialization of the JobHandler type. This exposes BuilderDescription as the job description + * type. + */ +export type BuilderJobHandler< + A extends json.JsonObject = json.JsonObject, + I extends BuilderInput = BuilderInput, + O extends BuilderOutput = BuilderOutput, +> = experimental.jobs.JobHandler & { jobDescription: BuilderDescription }; + +/** + * A Builder description, which is used internally. Adds the builder info which is the + * metadata attached to a builder in Architect. + */ +export interface BuilderDescription extends experimental.jobs.JobDescription { + info: BuilderInfo; +} + +/** + * A Builder instance. Use createBuilder() to create one of these. + */ +export interface Builder { + // A fully compatible job handler. + handler: experimental.jobs.JobHandler; + + // Metadata associated with this builder. + [BuilderSymbol]: true; + [BuilderVersionSymbol]: string; +} + +export interface ArchitectHost { + /** + * Get the builder name for a target. + * @param target The target to inspect. + */ + getBuilderNameForTarget(target: Target): Promise; + + /** + * Resolve a builder. This needs to return a string which will be used in a dynamic `import()` + * clause. This should throw if no builder can be found. The dynamic import will throw if + * it is unsupported. + * @param builderName The name of the builder to be used. + * @returns All the info needed for the builder itself. + */ + resolveBuilder(builderName: string): Promise; + loadBuilder(info: BuilderInfoT): Promise; + + getCurrentDirectory(): Promise; + getWorkspaceRoot(): Promise; + + getOptionsForTarget(target: Target): Promise; +} diff --git a/packages/angular_devkit/architect/src/output-schema.json b/packages/angular_devkit/architect/src/output-schema.json new file mode 100644 index 000000000000..8dbcc54add57 --- /dev/null +++ b/packages/angular_devkit/architect/src/output-schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "BuilderOutputSchema", + "title": "Output schema for builders.", + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "error": { + "type": "string" + }, + "target": { + "type": "object", + "properties": { + "project": { + "type": "string" + }, + "target": { + "type": "string" + }, + "configuration": { + "type": "string" + } + } + }, + "info": { + "type": "object" + } + }, + "required": [ + "success" + ] +} diff --git a/packages/angular_devkit/architect/src/progress-schema.json b/packages/angular_devkit/architect/src/progress-schema.json new file mode 100644 index 000000000000..24cd967a4f92 --- /dev/null +++ b/packages/angular_devkit/architect/src/progress-schema.json @@ -0,0 +1,102 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "BuilderProgressSchema", + "title": "Progress schema for builders.", + "type": "object", + "allOf": [ + { + "type": "object", + "oneOf": [ + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "stopped" + ] + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "waiting" + ] + }, + "status": { + "type": "string" + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "running" + ] + }, + "current": { + "type": "number", + "minimum": 0 + }, + "total": { + "type": "number", + "minimum": 0 + }, + "status": { + "type": "string" + } + }, + "required": [ + "state" + ] + }, + { + "type": "object", + "properties": { + "state": { + "type": "string", + "enum": [ + "error" + ] + }, + "error": true + }, + "required": [ + "state" + ] + } + ] + }, + { + "type": "object", + "properties": { + "builder": { + "type": "object" + }, + "target": { + "type": "object" + }, + "id": { + "type": "number" + } + }, + "required": [ + "builder", + "id" + ] + } + ] +} diff --git a/packages/angular_devkit/architect/src/schedule-by-name.ts b/packages/angular_devkit/architect/src/schedule-by-name.ts new file mode 100644 index 000000000000..d3bc12b07040 --- /dev/null +++ b/packages/angular_devkit/architect/src/schedule-by-name.ts @@ -0,0 +1,121 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental, json, logging } from '@angular-devkit/core'; +import { Subscription } from 'rxjs'; +import { first, ignoreElements, map, shareReplay } from 'rxjs/operators'; +import { + BuilderInfo, + BuilderInput, + BuilderOutput, BuilderProgressReport, + BuilderRun, + Target, + targetStringFromTarget, +} from './api'; + +const progressSchema = require('./progress-schema.json'); + + +let _uniqueId = 0; + +export async function scheduleByName( + name: string, + buildOptions: json.JsonObject, + options: { + target?: Target, + scheduler: experimental.jobs.Scheduler, + logger: logging.LoggerApi, + workspaceRoot: string | Promise, + currentDirectory: string | Promise, + }, +): Promise { + const childLoggerName = options.target ? `{${targetStringFromTarget(options.target)}}` : name; + const logger = options.logger.createChild(childLoggerName); + const job = options.scheduler.schedule<{}, BuilderInput, BuilderOutput>(name, {}); + let stateSubscription: Subscription; + + const workspaceRoot = await options.workspaceRoot; + const currentDirectory = await options.currentDirectory; + + const description = await job.description.toPromise(); + const info = description.info as BuilderInfo; + const id = ++_uniqueId; + + const message = { + id, + currentDirectory: workspaceRoot, + workspaceRoot: currentDirectory, + info: info, + options: buildOptions, + ...(options.target ? { target: options.target } : {}), + }; + + // Wait for the job to be ready. + if (job.state !== experimental.jobs.JobState.Started) { + stateSubscription = job.outboundBus.subscribe(event => { + if (event.kind === experimental.jobs.JobOutboundMessageKind.Start) { + job.input.next(message); + } + }); + } else { + job.input.next(message); + } + + const s = job.outboundBus.subscribe( + message => { + if (message.kind == experimental.jobs.JobOutboundMessageKind.Log) { + logger.log(message.entry.level, message.entry.message, message.entry); + } + }, + undefined, + () => { + s.unsubscribe(); + if (stateSubscription) { + stateSubscription.unsubscribe(); + } + }, + ); + const output = job.output.pipe( + map(output => ({ + ...output, + ...options.target ? { target: options.target } : 0, + info, + } as BuilderOutput)), + ); + + return { + id, + info, + result: output.pipe(first()).toPromise(), + output, + progress: job.getChannel('progress', progressSchema).pipe( + shareReplay(1), + ), + stop() { + job.stop(); + + return output.pipe(ignoreElements()).toPromise(); + }, + }; +} + +export async function scheduleByTarget( + target: Target, + overrides: json.JsonObject, + options: { + scheduler: experimental.jobs.Scheduler, + logger: logging.LoggerApi, + workspaceRoot: string | Promise, + currentDirectory: string | Promise, + }, +): Promise { + return scheduleByName(`{${targetStringFromTarget(target)}}`, overrides, { + ...options, + target, + logger: options.logger, + }); +} diff --git a/packages/angular_devkit/architect/testing/index2.ts b/packages/angular_devkit/architect/testing/index2.ts new file mode 100644 index 000000000000..783d596d1a54 --- /dev/null +++ b/packages/angular_devkit/architect/testing/index2.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export * from './testing-architect-host'; diff --git a/packages/angular_devkit/architect/testing/testing-architect-host.ts b/packages/angular_devkit/architect/testing/testing-architect-host.ts new file mode 100644 index 000000000000..1e4824aac36f --- /dev/null +++ b/packages/angular_devkit/architect/testing/testing-architect-host.ts @@ -0,0 +1,108 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { json } from '@angular-devkit/core'; +import { BuilderInfo, Target, targetStringFromTarget } from '../src/index2'; +import { ArchitectHost, Builder } from '../src/internal'; + +export class TestingArchitectHost implements ArchitectHost { + private _builderImportMap = new Map(); + private _builderMap = new Map(); + private _targetMap = new Map(); + + /** + * Can provide a backend host, in case of integration tests. + * @param workspaceRoot The workspace root to use. + * @param currentDirectory The current directory to use. + * @param _backendHost A host to defer calls that aren't resolved here. + */ + constructor( + public workspaceRoot = '', + public currentDirectory = workspaceRoot, + private _backendHost: ArchitectHost | null = null, + ) {} + + addBuilder( + builderName: string, + builder: Builder, + description = 'Testing only builder.', + optionSchema: json.schema.JsonSchema = { type: 'object' }, + ) { + this._builderImportMap.set(builderName, builder); + this._builderMap.set(builderName, { builderName, description, optionSchema }); + } + async addBuilderFromPackage(packageName: string) { + const packageJson = await import(packageName + '/package.json'); + if (!('builders' in packageJson)) { + throw new Error('Invalid package.json, builders key not found.'); + } + + const builderJsonPath = packageName + '/' + packageJson['builders']; + const builderJson = await import(builderJsonPath); + const builders = builderJson['builders']; + if (!builders) { + throw new Error('Invalid builders.json, builders key not found.'); + } + + for (const builderName of Object.keys(builders)) { + const b = builders[builderName]; + // TODO: remove this check as v1 is not supported anymore. + if (!b.implementation) { continue; } + const handler = await import(builderJsonPath + '/../' + b.implementation); + const optionsSchema = await import(builderJsonPath + '/../' + b.schema); + this.addBuilder(builderName, handler, b.description, optionsSchema); + } + } + addTarget(target: Target, builderName: string, options: json.JsonObject = {}) { + this._targetMap.set(targetStringFromTarget(target), { builderName, options }); + } + + async getBuilderNameForTarget(target: Target): Promise { + const name = targetStringFromTarget(target); + const maybeTarget = this._targetMap.get(name); + if (!maybeTarget) { + return this._backendHost && this._backendHost.getBuilderNameForTarget(target); + } + + return maybeTarget.builderName; + } + + /** + * Resolve a builder. This needs to return a string which will be used in a dynamic `import()` + * clause. This should throw if no builder can be found. The dynamic import will throw if + * it is unsupported. + * @param builderName The name of the builder to be used. + * @returns All the info needed for the builder itself. + */ + async resolveBuilder(builderName: string): Promise { + return this._builderMap.get(builderName) + || (this._backendHost && this._backendHost.resolveBuilder(builderName)); + } + + async getCurrentDirectory(): Promise { + return this.currentDirectory; + } + async getWorkspaceRoot(): Promise { + return this.workspaceRoot; + } + + async getOptionsForTarget(target: Target): Promise { + const name = targetStringFromTarget(target); + const maybeTarget = this._targetMap.get(name); + if (!maybeTarget) { + return this._backendHost && this._backendHost.getOptionsForTarget(target); + } + + return maybeTarget.options; + } + + async loadBuilder(info: BuilderInfo): Promise { + return this._builderImportMap.get(info.builderName) + || (this._backendHost && this._backendHost.loadBuilder(info)); + } + +} From ed571a3039e34f33292e717fb9fe0d8cc9f35d5b Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:52:07 -0800 Subject: [PATCH 06/11] feat(@angular-devkit/architect): add node architect host This host resolves using the package resolution and reading the targets from the workspace API. --- packages/angular_devkit/architect/BUILD | 25 ++++ .../angular_devkit/architect/node/index.ts | 8 ++ .../node/node-modules-architect-host.ts | 109 ++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 packages/angular_devkit/architect/node/index.ts create mode 100644 packages/angular_devkit/architect/node/node-modules-architect-host.ts diff --git a/packages/angular_devkit/architect/BUILD b/packages/angular_devkit/architect/BUILD index 98e2432d239d..e54ab10149c2 100644 --- a/packages/angular_devkit/architect/BUILD +++ b/packages/angular_devkit/architect/BUILD @@ -28,6 +28,31 @@ ts_json_schema( src = "src/progress-schema.json", ) +ts_library( + name = "node", + srcs = glob( + include = ["node/**/*.ts"], + exclude = [ + "**/*_spec.ts", + "**/*_spec_large.ts", + ], + ), + module_name = "@angular-devkit/architect/node", + module_root = "node/index.d.ts", + # strict_checks = False, + deps = [ + "//packages/angular_devkit/core", + "//packages/angular_devkit/core:node", + "@rxjs", + "@rxjs//operators", + "@npm//@types/node", + ":architect", + ":builder_builders_schema", + ":builder_input_schema", + ":builder_output_schema", + ], +) + ts_library( name = "architect", srcs = glob( diff --git a/packages/angular_devkit/architect/node/index.ts b/packages/angular_devkit/architect/node/index.ts new file mode 100644 index 000000000000..721047f44093 --- /dev/null +++ b/packages/angular_devkit/architect/node/index.ts @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +export * from './node-modules-architect-host'; diff --git a/packages/angular_devkit/architect/node/node-modules-architect-host.ts b/packages/angular_devkit/architect/node/node-modules-architect-host.ts new file mode 100644 index 000000000000..7574f0e78eed --- /dev/null +++ b/packages/angular_devkit/architect/node/node-modules-architect-host.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { experimental, json } from '@angular-devkit/core'; +import { resolve } from '@angular-devkit/core/node'; +import * as path from 'path'; +import { Schema as BuilderSchema } from '../src/builders-schema'; +import { BuilderInfo } from '../src/index2'; +import { Target } from '../src/input-schema'; +import { ArchitectHost, Builder, BuilderSymbol } from '../src/internal'; + + +export type NodeModulesBuilderInfo = BuilderInfo & { + import: string; +}; + + +// TODO: create a base class for all workspace related hosts. +export class WorkspaceNodeModulesArchitectHost implements ArchitectHost { + constructor( + protected _workspace: experimental.workspace.Workspace, + protected _root: string, + ) {} + + async getBuilderNameForTarget(target: Target) { + return this._workspace.getProjectTargets(target.project)[target.target]['builder']; + } + + /** + * Resolve a builder. This needs to be a string which will be used in a dynamic `import()` + * clause. This should throw if no builder can be found. The dynamic import will throw if + * it is unsupported. + * @param builderStr The name of the builder to be used. + * @returns All the info needed for the builder itself. + */ + resolveBuilder(builderStr: string): Promise { + const [packageName, builderName] = builderStr.split(':', 2); + if (!builderName) { + throw new Error('No builder name specified.'); + } + + const packageJsonPath = resolve(packageName, { + basedir: this._root, + checkLocal: true, + checkGlobal: true, + resolvePackageJson: true, + }); + + const packageJson = require(packageJsonPath); + if (!packageJson['builders']) { + throw new Error(`Package ${JSON.stringify(packageName)} has no builders defined.`); + } + + const builderJsonPath = path.resolve(path.dirname(packageJsonPath), packageJson['builders']); + const builderJson = require(builderJsonPath) as BuilderSchema; + + const builder = builderJson.builders && builderJson.builders[builderName]; + + if (!builder) { + throw new Error(`Cannot find builder ${JSON.stringify(builderName)}.`); + } + + const importPath = builder.implementation; + if (!importPath) { + throw new Error('Invalid builder JSON'); + } + + return Promise.resolve({ + name: builderStr, + builderName, + description: builder['description'], + optionSchema: require(path.resolve(path.dirname(builderJsonPath), builder.schema)), + import: path.resolve(path.dirname(builderJsonPath), importPath), + }); + } + + async getCurrentDirectory() { + return process.cwd(); + } + + async getWorkspaceRoot() { + return this._root; + } + + async getOptionsForTarget(target: Target): Promise { + const targetSpec = this._workspace.getProjectTargets(target.project)[target.target]; + if (target.configuration && !targetSpec['configurations']) { + throw new Error('Configuration not set in the workspace.'); + } + + return { + ...targetSpec['options'], + ...(target.configuration ? targetSpec['configurations'][target.configuration] : 0), + }; + } + + async loadBuilder(info: NodeModulesBuilderInfo): Promise { + const builder = (await import(info.import)).default; + if (builder[BuilderSymbol]) { + return builder; + } + + throw new Error('Builder is not a builder'); + } +} From 5dc47f22b86513de8665d4d55e7bebd9189a8ce2 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:54:56 -0800 Subject: [PATCH 07/11] feat(@angular-devkit/architect): add generic architect builders Four builders were added; - true, always succeed - false, always fails - concat, runs all targets or builders in succession - allOf, runs all targets or builders in parallel --- .../architect/builders/all-of.ts | 59 +++++++++++++++++++ .../architect/builders/builders.json | 25 ++++++++ .../architect/builders/concat.ts | 56 ++++++++++++++++++ .../architect/builders/false.ts | 14 +++++ .../architect/builders/noop-schema.json | 4 ++ .../architect/builders/operator-schema.json | 45 ++++++++++++++ .../angular_devkit/architect/builders/true.ts | 11 ++++ .../angular_devkit/architect/package.json | 5 +- 8 files changed, 217 insertions(+), 2 deletions(-) create mode 100644 packages/angular_devkit/architect/builders/all-of.ts create mode 100644 packages/angular_devkit/architect/builders/builders.json create mode 100644 packages/angular_devkit/architect/builders/concat.ts create mode 100644 packages/angular_devkit/architect/builders/false.ts create mode 100644 packages/angular_devkit/architect/builders/noop-schema.json create mode 100644 packages/angular_devkit/architect/builders/operator-schema.json create mode 100644 packages/angular_devkit/architect/builders/true.ts diff --git a/packages/angular_devkit/architect/builders/all-of.ts b/packages/angular_devkit/architect/builders/all-of.ts new file mode 100644 index 000000000000..491bdd682322 --- /dev/null +++ b/packages/angular_devkit/architect/builders/all-of.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { json } from '@angular-devkit/core'; +import { EMPTY, from, of } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; +import { BuilderOutput, BuilderRun, createBuilder } from '../src/index2'; +import { Schema as OperatorSchema } from './operator-schema'; + +export default createBuilder((options, context) => { + const allRuns: Promise<[number, BuilderRun]>[] = []; + + context.reportProgress(0, + (options.targets ? options.targets.length : 0) + + (options.builders ? options.builders.length : 0), + ); + + if (options.targets) { + allRuns.push(...options.targets.map(({ target: targetStr, overrides }, i) => { + const [project, target, configuration] = targetStr.split(/:/g, 3); + + return context.scheduleTarget({ project, target, configuration }, overrides || {}) + .then(run => [i, run] as [number, BuilderRun]); + })); + } + + if (options.builders) { + allRuns.push(...options.builders.map(({ builder, options }, i) => { + return context.scheduleBuilder(builder, options || {}) + .then(run => [i, run] as [number, BuilderRun]); + })); + } + + const allResults: (BuilderOutput | null)[] = allRuns.map(() => null); + let n = 0; + context.reportProgress(n++, allRuns.length); + + return from(allRuns).pipe( + mergeMap(runPromise => from(runPromise)), + mergeMap(([i, run]: [number, BuilderRun]) => run.output.pipe(map(output => [i, output]))), + mergeMap<[number, BuilderOutput], BuilderOutput>(([i, output]) => { + allResults[i] = output; + context.reportProgress(n++, allRuns.length); + + if (allResults.some(x => x === null)) { + // Some builders aren't done running yet. + return EMPTY; + } else { + return of({ + success: allResults.every(x => x ? x.success : false), + }); + } + }), + ); +}); diff --git a/packages/angular_devkit/architect/builders/builders.json b/packages/angular_devkit/architect/builders/builders.json new file mode 100644 index 000000000000..5bbff8289447 --- /dev/null +++ b/packages/angular_devkit/architect/builders/builders.json @@ -0,0 +1,25 @@ +{ + "$schema": "../src/builders-schema.json", + "builders": { + "true": { + "implementation": "./true", + "schema": "./noop-schema.json", + "description": "Always succeed." + }, + "false": { + "implementation": "./false", + "schema": "./noop-schema.json", + "description": "Always fails." + }, + "allOf": { + "implementation": "./all-of", + "schema": "./operator-schema.json", + "description": "A builder that executes many builders in parallel, and succeed if both succeeds." + }, + "concat": { + "implementation": "./concat", + "schema": "./operator-schema.json", + "description": "A builder that executes many builders one after the other, and stops when one fail. It will succeed if all builders succeeds (and return the last output)" + } + } +} diff --git a/packages/angular_devkit/architect/builders/concat.ts b/packages/angular_devkit/architect/builders/concat.ts new file mode 100644 index 000000000000..53ed963be610 --- /dev/null +++ b/packages/angular_devkit/architect/builders/concat.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { json } from '@angular-devkit/core'; +import { from, of } from 'rxjs'; +import { concatMap, first, last, map, switchMap } from 'rxjs/operators'; +import { BuilderOutput, BuilderRun, createBuilder } from '../src/index2'; +import { Schema as OperatorSchema } from './operator-schema'; + +export default createBuilder((options, context) => { + const allRuns: (() => Promise)[] = []; + + context.reportProgress(0, + (options.targets ? options.targets.length : 0) + + (options.builders ? options.builders.length : 0), + ); + + if (options.targets) { + allRuns.push(...options.targets.map(({ target: targetStr, overrides }) => { + const [project, target, configuration] = targetStr.split(/:/g, 3); + + return () => context.scheduleTarget({ project, target, configuration }, overrides || {}); + })); + } + + if (options.builders) { + allRuns.push(...options.builders.map(({ builder, options }) => { + return () => context.scheduleBuilder(builder, options || {}); + })); + } + + let stop: BuilderOutput | null = null; + let i = 0; + context.reportProgress(i++, allRuns.length); + + return from(allRuns).pipe( + concatMap(fn => stop ? of(null) : from(fn()).pipe( + switchMap(run => run === null ? of(null) : run.output.pipe(first())), + )), + map(output => { + context.reportProgress(i++, allRuns.length); + if (output === null || stop !== null) { + return stop || { success: false }; + } else if (output.success === false) { + return stop = output; + } else { + return output; + } + }), + last(), + ); +}); diff --git a/packages/angular_devkit/architect/builders/false.ts b/packages/angular_devkit/architect/builders/false.ts new file mode 100644 index 000000000000..4c980ae57752 --- /dev/null +++ b/packages/angular_devkit/architect/builders/false.ts @@ -0,0 +1,14 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { of } from 'rxjs'; +import { createBuilder } from '../src/index2'; + +export default createBuilder(() => of({ + success: false, + error: 'False builder always errors.', +})); diff --git a/packages/angular_devkit/architect/builders/noop-schema.json b/packages/angular_devkit/architect/builders/noop-schema.json new file mode 100644 index 000000000000..afadf0925f37 --- /dev/null +++ b/packages/angular_devkit/architect/builders/noop-schema.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json-schema.org/schema", + "type": "object" +} \ No newline at end of file diff --git a/packages/angular_devkit/architect/builders/operator-schema.json b/packages/angular_devkit/architect/builders/operator-schema.json new file mode 100644 index 000000000000..8519c83ee7d4 --- /dev/null +++ b/packages/angular_devkit/architect/builders/operator-schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/schema", + "description": "All input types of builders that perform operations on one or multiple sub-builders.", + "type": "object", + "properties": { + "builders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "builder": { + "type": "string", + "pattern": ".*:.*" + }, + "options": { + "type": "object" + } + }, + "required": [ + "builder" + ] + }, + "minItems": 1 + }, + "targets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "target": { + "type": "string", + "pattern": ".*:.*" + }, + "overrides": { + "type": "object" + } + }, + "required": [ + "target" + ] + }, + "minItems": 1 + } + } +} diff --git a/packages/angular_devkit/architect/builders/true.ts b/packages/angular_devkit/architect/builders/true.ts new file mode 100644 index 000000000000..cbb1e2ee55a3 --- /dev/null +++ b/packages/angular_devkit/architect/builders/true.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { of } from 'rxjs'; +import { createBuilder } from '../src/index2'; + +export default createBuilder(() => of({ success: true })); diff --git a/packages/angular_devkit/architect/package.json b/packages/angular_devkit/architect/package.json index f04b3a985ffa..1a5c8b0c3c93 100644 --- a/packages/angular_devkit/architect/package.json +++ b/packages/angular_devkit/architect/package.json @@ -7,5 +7,6 @@ "dependencies": { "@angular-devkit/core": "0.0.0", "rxjs": "6.3.3" - } -} \ No newline at end of file + }, + "builders": "./builders/builders.json" +} From d2e72736f4baf84331893913d8caa42eea8dab4c Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:57:14 -0800 Subject: [PATCH 08/11] feat(@angular-devkit/build-angular): move tslint to new API It is only new files and the old builder is still available. The new one can only be used by the new Architect API. --- .../build_angular/builders.json | 1 + .../build_angular/src/tslint/index2.ts | 234 +++++++++++++++ .../test/tslint/works2_spec_large.ts | 278 ++++++++++++++++++ 3 files changed, 513 insertions(+) create mode 100644 packages/angular_devkit/build_angular/src/tslint/index2.ts create mode 100644 packages/angular_devkit/build_angular/test/tslint/works2_spec_large.ts diff --git a/packages/angular_devkit/build_angular/builders.json b/packages/angular_devkit/build_angular/builders.json index bf8b1a6ca284..6d299c5751b3 100644 --- a/packages/angular_devkit/build_angular/builders.json +++ b/packages/angular_devkit/build_angular/builders.json @@ -32,6 +32,7 @@ "description": "Run protractor over a dev server." }, "tslint": { + "implementation": "./src/tslint/index2", "class": "./src/tslint", "schema": "./src/tslint/schema.json", "description": "Run tslint over a TS project." diff --git a/packages/angular_devkit/build_angular/src/tslint/index2.ts b/packages/angular_devkit/build_angular/src/tslint/index2.ts new file mode 100644 index 000000000000..c301628db64e --- /dev/null +++ b/packages/angular_devkit/build_angular/src/tslint/index2.ts @@ -0,0 +1,234 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect/src/index2'; +import { json } from '@angular-devkit/core'; +import { readFileSync } from 'fs'; +import * as glob from 'glob'; +import { Minimatch } from 'minimatch'; +import * as path from 'path'; +import * as tslint from 'tslint'; // tslint:disable-line:no-implicit-dependencies +import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies +import { stripBom } from '../angular-cli-files/utilities/strip-bom'; +import { Schema as RealTslintBuilderOptions } from './schema'; + + +type TslintBuilderOptions = RealTslintBuilderOptions & json.JsonObject; + + +async function _loadTslint() { + let tslint; + try { + tslint = await import('tslint'); // tslint:disable-line:no-implicit-dependencies + } catch { + throw new Error('Unable to find TSLint. Ensure TSLint is installed.'); + } + + const version = tslint.Linter.VERSION && tslint.Linter.VERSION.split('.'); + if (!version || version.length < 2 || Number(version[0]) < 5 || Number(version[1]) < 5) { + throw new Error('TSLint must be version 5.5 or higher.'); + } + + return tslint; +} + + +async function _run(config: TslintBuilderOptions, context: BuilderContext): Promise { + const systemRoot = context.workspaceRoot; + process.chdir(context.currentDirectory); + const options = config; + const projectName = context.target && context.target.project || ''; + + // Print formatter output only for non human-readable formats. + const printInfo = ['prose', 'verbose', 'stylish'].includes(options.format || '') + && !options.silent; + + context.reportStatus(`Linting ${JSON.stringify(projectName)}...`); + if (printInfo) { + context.logger.info(`Linting ${JSON.stringify(projectName)}...`); + } + + if (!options.tsConfig && options.typeCheck) { + throw new Error('A "project" must be specified to enable type checking.'); + } + + const projectTslint = await _loadTslint(); + const tslintConfigPath = options.tslintConfig + ? path.resolve(systemRoot, options.tslintConfig) + : null; + const Linter = projectTslint.Linter; + + let result: undefined | tslint.LintResult = undefined; + if (options.tsConfig) { + const tsConfigs = Array.isArray(options.tsConfig) ? options.tsConfig : [options.tsConfig]; + context.reportProgress(0, tsConfigs.length); + const allPrograms = tsConfigs.map(tsConfig => { + return Linter.createProgram(path.resolve(systemRoot, tsConfig)); + }); + + let i = 0; + for (const program of allPrograms) { + const partial + = await _lint(projectTslint, systemRoot, tslintConfigPath, options, program, allPrograms); + if (result === undefined) { + result = partial; + } else { + result.failures = result.failures + .filter(curr => { + return !partial.failures.some(prev => curr.equals(prev)); + }) + .concat(partial.failures); + + // we are not doing much with 'errorCount' and 'warningCount' + // apart from checking if they are greater than 0 thus no need to dedupe these. + result.errorCount += partial.errorCount; + result.warningCount += partial.warningCount; + + if (partial.fixes) { + result.fixes = result.fixes ? result.fixes.concat(partial.fixes) : partial.fixes; + } + } + + context.reportProgress(++i, allPrograms.length); + } + } else { + result = await _lint(projectTslint, systemRoot, tslintConfigPath, options); + } + + if (result == undefined) { + throw new Error('Invalid lint configuration. Nothing to lint.'); + } + + if (!options.silent) { + const Formatter = projectTslint.findFormatter(options.format || ''); + if (!Formatter) { + throw new Error(`Invalid lint format "${options.format}".`); + } + const formatter = new Formatter(); + + const output = formatter.format(result.failures, result.fixes); + if (output.trim()) { + context.logger.info(output); + } + } + + if (result.warningCount > 0 && printInfo) { + context.logger.warn('Lint warnings found in the listed files.'); + } + + if (result.errorCount > 0 && printInfo) { + context.logger.error('Lint errors found in the listed files.'); + } + + if (result.warningCount === 0 && result.errorCount === 0 && printInfo) { + context.logger.info('All files pass linting.'); + } + + return { + success: options.force || result.errorCount === 0, + }; +} + + +export default createBuilder(_run); + + +async function _lint( + projectTslint: typeof tslint, + systemRoot: string, + tslintConfigPath: string | null, + options: TslintBuilderOptions, + program?: ts.Program, + allPrograms?: ts.Program[], +) { + const Linter = projectTslint.Linter; + const Configuration = projectTslint.Configuration; + + const files = getFilesToLint(systemRoot, options, Linter, program); + const lintOptions = { + fix: !!options.fix, + formatter: options.format, + }; + + const linter = new Linter(lintOptions, program); + + let lastDirectory: string | undefined = undefined; + let configLoad; + for (const file of files) { + if (program && allPrograms) { + // If it cannot be found in ANY program, then this is an error. + if (allPrograms.every(p => p.getSourceFile(file) === undefined)) { + throw new Error( + `File ${JSON.stringify(file)} is not part of a TypeScript project '${options.tsConfig}'.`, + ); + } else if (program.getSourceFile(file) === undefined) { + // The file exists in some other programs. We will lint it later (or earlier) in the loop. + continue; + } + } + + const contents = getFileContents(file); + + // Only check for a new tslint config if the path changes. + const currentDirectory = path.dirname(file); + if (currentDirectory !== lastDirectory) { + configLoad = Configuration.findConfiguration(tslintConfigPath, file); + lastDirectory = currentDirectory; + } + + if (configLoad) { + // Give some breathing space to other promises that might be waiting. + await Promise.resolve(); + linter.lint(file, contents, configLoad.results); + } + } + + return linter.getResult(); +} + +function getFilesToLint( + root: string, + options: TslintBuilderOptions, + linter: typeof tslint.Linter, + program?: ts.Program, +): string[] { + const ignore = options.exclude; + const files = options.files || []; + + if (files.length > 0) { + return files + .map(file => glob.sync(file, { cwd: root, ignore, nodir: true })) + .reduce((prev, curr) => prev.concat(curr), []) + .map(file => path.join(root, file)); + } + + if (!program) { + return []; + } + + let programFiles = linter.getFileNames(program); + + if (ignore && ignore.length > 0) { + // normalize to support ./ paths + const ignoreMatchers = ignore + .map(pattern => new Minimatch(path.normalize(pattern), { dot: true })); + + programFiles = programFiles + .filter(file => !ignoreMatchers.some(matcher => matcher.match(path.relative(root, file)))); + } + + return programFiles; +} + +function getFileContents(file: string): string { + // NOTE: The tslint CLI checks for and excludes MPEG transport streams; this does not. + try { + return stripBom(readFileSync(file, 'utf-8')); + } catch { + throw new Error(`Could not read file '${file}'.`); + } +} diff --git a/packages/angular_devkit/build_angular/test/tslint/works2_spec_large.ts b/packages/angular_devkit/build_angular/test/tslint/works2_spec_large.ts new file mode 100644 index 000000000000..4fcb9d15916f --- /dev/null +++ b/packages/angular_devkit/build_angular/test/tslint/works2_spec_large.ts @@ -0,0 +1,278 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { Architect, Target } from '@angular-devkit/architect/src/index2'; +import { TestingArchitectHost } from '@angular-devkit/architect/testing/index2'; +import { experimental, join, logging, normalize, schema } from '@angular-devkit/core'; +import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import * as fs from 'fs'; +import * as path from 'path'; + +const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any +const workspaceRoot = join(devkitRoot, 'tests/angular_devkit/build_angular/hello-world-app/'); +const lintTarget: Target = { project: 'app', target: 'lint' }; + +// tslint:disable-next-line:no-big-function +describe('Tslint Target', () => { + // const filesWithErrors = { 'src/foo.ts': 'const foo = "";\n' }; + let testArchitectHost: TestingArchitectHost; + let architect: Architect; + + beforeEach(async () => { + const vfHost = new NodeJsSyncHost(); + const configContent = fs.readFileSync(path.join(workspaceRoot, 'angular.json'), 'utf-8'); + const workspaceJson = JSON.parse(configContent); + + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + + const workspace = new experimental.workspace.Workspace(workspaceRoot, vfHost); + await workspace.loadWorkspaceFromJson(workspaceJson).toPromise(); + + testArchitectHost = new TestingArchitectHost( + workspaceRoot, workspaceRoot, + new WorkspaceNodeModulesArchitectHost(workspace, workspaceRoot), + ); + architect = new Architect(testArchitectHost, registry); + }); + + it('works', async () => { + const run = await architect.scheduleTarget({ project: 'app', target: 'lint' }); + const output = await run.result; + expect(output.success).toBe(true); + await run.stop(); + }, 30000); + + it(`should show project name as status and in the logs`, async () => { + // Check logs. + const logger = new logging.Logger('lint-info'); + const allLogs: string[] = []; + logger.subscribe(entry => allLogs.push(entry.message)); + + const run = await architect.scheduleTarget(lintTarget, {}, { logger }); + + // Check status updates. + const allStatus: string[] = []; + run.progress.subscribe(progress => { + if (progress.status !== undefined) { + allStatus.push(progress.status); + } + }); + + const output = await run.result; + expect(output.success).toBe(true); + expect(allStatus).toContain(jasmine.stringMatching(/linting.*"app".*/i)); + expect(allLogs).toContain(jasmine.stringMatching(/linting.*"app".*/i)); + await run.stop(); + }); + + it(`should not show project name when formatter is non human readable`, async () => { + const overrides = { + format: 'checkstyle', + }; + + // Check logs. + const logger = new logging.Logger('lint-info'); + const allLogs: string[] = []; + logger.subscribe(entry => allLogs.push(entry.message)); + + const run = await architect.scheduleTarget(lintTarget, overrides, { logger }); + + // Check status updates. + const allStatus: string[] = []; + run.progress.subscribe(progress => { + if (progress.status !== undefined) { + allStatus.push(progress.status); + } + }); + + const output = await run.result; + expect(output.success).toBe(true); + expect(allStatus).toContain(jasmine.stringMatching(/linting.*"app".*/i)); + expect(allLogs).not.toContain(jasmine.stringMatching(/linting.*"app".*/i)); + await run.stop(); + }, 30000); + + // it('should report lint error once', (done) => { + // host.writeMultipleFiles({'src/app/app.component.ts': 'const foo = "";\n' }); + // const logger = new TestLogger('lint-error'); + // + // runTargetSpec(host, tslintTargetSpec, undefined, DefaultTimeout, logger).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(false)), + // tap(() => { + // // this is to make sure there are no duplicates + // expect(logger.includes(`" should be \'\nERROR`)).toBe(false); + // + // expect(logger.includes(`" should be '`)).toBe(true); + // expect(logger.includes(`Lint errors found in the listed files`)).toBe(true); + // }), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports exclude with glob', (done) => { + // host.writeMultipleFiles(filesWithErrors); + // const overrides: Partial = { exclude: ['**/foo.ts'] }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports exclude with relative paths', (done) => { + // host.writeMultipleFiles(filesWithErrors); + // const overrides: Partial = { exclude: ['src/foo.ts'] }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it(`supports exclude with paths starting with './'`, (done) => { + // host.writeMultipleFiles(filesWithErrors); + // const overrides: Partial = { exclude: ['./src/foo.ts'] }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports fix', (done) => { + // host.writeMultipleFiles(filesWithErrors); + // const overrides: Partial = { fix: true }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // tap(() => { + // const fileName = normalize('src/foo.ts'); + // const content = virtualFs.fileBufferToString(host.scopedSync().read(fileName)); + // expect(content).toContain(`const foo = '';`); + // }), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports force', (done) => { + // host.writeMultipleFiles(filesWithErrors); + // const logger = new TestLogger('lint-force'); + // const overrides: Partial = { force: true }; + // + // runTargetSpec(host, tslintTargetSpec, overrides, DefaultTimeout, logger).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // tap(() => { + // expect(logger.includes(`" should be '`)).toBe(true); + // expect(logger.includes(`Lint errors found in the listed files`)).toBe(true); + // }), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports format', (done) => { + // host.writeMultipleFiles(filesWithErrors); + // const logger = new TestLogger('lint-format'); + // const overrides: Partial = { format: 'stylish' }; + // + // runTargetSpec(host, tslintTargetSpec, overrides, DefaultTimeout, logger).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(false)), + // tap(() => { + // expect(logger.includes(`quotemark`)).toBe(true); + // }), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports finding configs', (done) => { + // host.writeMultipleFiles({ + // 'src/app/foo/foo.ts': `const foo = '';\n`, + // 'src/app/foo/tslint.json': ` + // { + // "rules": { + // "quotemark": [ + // true, + // "double" + // ] + // } + // } + // `, + // }); + // const overrides: Partial = { tslintConfig: undefined }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(false)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports overriding configs', (done) => { + // host.writeMultipleFiles({ + // 'src/app/foo/foo.ts': `const foo = '';\n`, + // 'src/app/foo/tslint.json': ` + // { + // "rules": { + // "quotemark": [ + // true, + // "double" + // ] + // } + // } + // `, + // }); + // const overrides: Partial = { tslintConfig: 'tslint.json' }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports using files with no project', (done) => { + // const overrides: Partial = { + // tsConfig: undefined, + // files: ['src/app/**/*.ts'], + // }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports using one project as a string', (done) => { + // const overrides: Partial = { + // tsConfig: 'src/tsconfig.app.json', + // }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports using one project as an array', (done) => { + // const overrides: Partial = { + // tsConfig: ['src/tsconfig.app.json'], + // }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('supports using two projects', (done) => { + // const overrides: Partial = { + // tsConfig: ['src/tsconfig.app.json', 'src/tsconfig.spec.json'], + // }; + // + // runTargetSpec(host, tslintTargetSpec, overrides).pipe( + // tap((buildEvent) => expect(buildEvent.success).toBe(true)), + // ).toPromise().then(done, done.fail); + // }, 30000); + // + // it('errors when type checking is used without a project', (done) => { + // const overrides: Partial = { + // tsConfig: undefined, + // typeCheck: true, + // }; + // + // runTargetSpec(host, tslintTargetSpec, overrides) + // .subscribe(undefined, () => done(), done.fail); + // }, 30000); +}); From 4db06c64a2d64be19588feb34a2d0f96241613fc Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 13 Feb 2019 11:58:29 -0800 Subject: [PATCH 09/11] feat(@angular-devkit/architect-cli): CLI tool to use new Architect API Move the entire Architect CLI to use the new API, and report progress using a progress bar for each worker currently executing. Shows log at the end of the execution. This is meant to be used as a debugging tool to help people move their builders to the new API. --- package.json | 1 + packages/angular_devkit/architect_cli/BUILD | 4 +- .../architect_cli/bin/architect.ts | 239 ++++++++++++------ .../angular_devkit/architect_cli/package.json | 9 +- .../architect_cli/src/progress.ts | 98 +++++++ yarn.lock | 52 +++- 6 files changed, 320 insertions(+), 83 deletions(-) create mode 100644 packages/angular_devkit/architect_cli/src/progress.ts diff --git a/package.json b/package.json index b58e937118a3..1c9a03e88c25 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ ] }, "dependencies": { + "@types/progress": "^2.0.3", "glob": "^7.0.3", "node-fetch": "^2.2.0", "puppeteer": "1.12.2", diff --git a/packages/angular_devkit/architect_cli/BUILD b/packages/angular_devkit/architect_cli/BUILD index 225116f0cf1d..d7d12f885ea4 100644 --- a/packages/angular_devkit/architect_cli/BUILD +++ b/packages/angular_devkit/architect_cli/BUILD @@ -12,15 +12,17 @@ ts_library( name = "architect_cli", srcs = [ "bin/architect.ts", - ], + ] + glob(["src/**/*.ts"]), module_name = "@angular-devkit/architect-cli", deps = [ "//packages/angular_devkit/architect", + "//packages/angular_devkit/architect:node", "//packages/angular_devkit/core", "//packages/angular_devkit/core:node", "@rxjs", "@rxjs//operators", "@npm//@types/node", "@npm//@types/minimist", + "@npm//@types/progress", ], ) diff --git a/packages/angular_devkit/architect_cli/bin/architect.ts b/packages/angular_devkit/architect_cli/bin/architect.ts index 36aa58b7f53e..4af723751d42 100644 --- a/packages/angular_devkit/architect_cli/bin/architect.ts +++ b/packages/angular_devkit/architect_cli/bin/architect.ts @@ -6,18 +6,22 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import 'symbol-observable'; -// symbol polyfill must go first -// tslint:disable-next-line:ordered-imports import-groups -import { Architect } from '@angular-devkit/architect'; -import { dirname, experimental, normalize, tags } from '@angular-devkit/core'; +import { index2 } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { + dirname, + experimental, + json, + logging, + normalize, + schema, + tags, terminal, +} from '@angular-devkit/core'; import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; import { existsSync, readFileSync } from 'fs'; import * as minimist from 'minimist'; import * as path from 'path'; -import { throwError } from 'rxjs'; -import { concatMap } from 'rxjs/operators'; +import { MultiProgressBar } from '../src/progress'; function findUp(names: string | string[], from: string) { @@ -44,7 +48,7 @@ function findUp(names: string | string[], from: string) { /** * Show usage of the CLI tool, and exit the process. */ -function usage(exitCode = 0): never { +function usage(logger: logging.Logger, exitCode = 0): never { logger.info(tags.stripIndent` architect [project][:target][:configuration] [options, ...] @@ -63,86 +67,165 @@ function usage(exitCode = 0): never { throw 0; // The node typing sometimes don't have a never type for process.exit(). } -/** Parse the command line. */ -const argv = minimist(process.argv.slice(2), { boolean: ['help'] }); +function _targetStringFromTarget({project, target, configuration}: index2.Target) { + return `${project}:${target}${configuration !== undefined ? ':' + configuration : ''}`; +} -/** Create the DevKit Logger used through the CLI. */ -const logger = createConsoleLogger(argv['verbose']); -// Check the target. -const targetStr = argv._.shift(); -if (!targetStr && argv.help) { - // Show architect usage if there's no target. - usage(); +interface BarInfo { + status?: string; + builder: index2.BuilderInfo; + target?: index2.Target; } -// Split a target into its parts. -let project: string, targetName: string, configuration: string; -if (targetStr) { - [project, targetName, configuration] = targetStr.split(':'); -} -// Load workspace configuration file. -const currentPath = process.cwd(); -const configFileNames = [ - 'angular.json', - '.angular.json', - 'workspace.json', - '.workspace.json', -]; - -const configFilePath = findUp(configFileNames, currentPath); - -if (!configFilePath) { - logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in ` - + `'${currentPath}' or in parent directories.`); - process.exit(3); - throw 3; // TypeScript doesn't know that process.exit() never returns. +async function _executeTarget( + parentLogger: logging.Logger, + workspace: experimental.workspace.Workspace, + root: string, + argv: minimist.ParsedArgs, + registry: json.schema.SchemaRegistry, +) { + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, root); + const architect = new index2.Architect(architectHost, registry); + + // Split a target into its parts. + const targetStr = argv._.shift() || ''; + const [project, target, configuration] = targetStr.split(':'); + const targetSpec = { project, target, configuration }; + + delete argv['help']; + delete argv['_']; + + const logger = new logging.Logger('jobs'); + const logs: logging.LogEntry[] = []; + logger.subscribe(entry => logs.push({ ...entry, message: `${entry.name}: ` + entry.message })); + + const run = await architect.scheduleTarget(targetSpec, argv, { logger }); + const bars = new MultiProgressBar(':name :bar (:current/:total) :status'); + + run.progress.subscribe( + update => { + const data = bars.get(update.id) || { + id: update.id, + builder: update.builder, + target: update.target, + status: update.status || '', + name: ((update.target ? _targetStringFromTarget(update.target) : update.builder.name) + + ' '.repeat(80) + ).substr(0, 40), + }; + + if (update.status !== undefined) { + data.status = update.status; + } + + switch (update.state) { + case index2.BuilderProgressState.Error: + data.status = 'Error: ' + update.error; + bars.update(update.id, data); + break; + + case index2.BuilderProgressState.Stopped: + data.status = 'Done.'; + bars.complete(update.id); + bars.update(update.id, data, update.total, update.total); + break; + + case index2.BuilderProgressState.Waiting: + bars.update(update.id, data); + break; + + case index2.BuilderProgressState.Running: + bars.update(update.id, data, update.current, update.total); + break; + } + + bars.render(); + }, + ); + + // Wait for full completion of the builder. + try { + const result = await run.result; + + if (result.success) { + parentLogger.info(terminal.green('SUCCESS')); + } else { + parentLogger.info(terminal.yellow('FAILURE')); + } + + parentLogger.info('\nLogs:'); + logs.forEach(l => parentLogger.next(l)); + + await run.stop(); + bars.terminate(); + + return result.success ? 0 : 1; + } catch (err) { + parentLogger.info(terminal.red('ERROR')); + parentLogger.info('\nLogs:'); + logs.forEach(l => parentLogger.next(l)); + + parentLogger.fatal('Exception:'); + parentLogger.fatal(err.stack); + + return 2; + } } -const root = dirname(normalize(configFilePath)); -const configContent = readFileSync(configFilePath, 'utf-8'); -const workspaceJson = JSON.parse(configContent); -const host = new NodeJsSyncHost(); -const workspace = new experimental.workspace.Workspace(root, host); +async function main(args: string[]): Promise { + /** Parse the command line. */ + const argv = minimist(args, { boolean: ['help'] }); -let lastBuildEvent = { success: true }; + /** Create the DevKit Logger used through the CLI. */ + const logger = createConsoleLogger(argv['verbose']); -workspace.loadWorkspaceFromJson(workspaceJson).pipe( - concatMap(ws => new Architect(ws).loadArchitect()), - concatMap(architect => { + // Check the target. + const targetStr = argv._[0] || ''; + if (!targetStr || argv.help) { + // Show architect usage if there's no target. + usage(logger); + } - const overrides = { ...argv }; - delete overrides['help']; - delete overrides['_']; + // Load workspace configuration file. + const currentPath = process.cwd(); + const configFileNames = [ + 'angular.json', + '.angular.json', + 'workspace.json', + '.workspace.json', + ]; - const targetSpec = { - project, - target: targetName, - configuration, - overrides, - }; + const configFilePath = findUp(configFileNames, currentPath); - // TODO: better logging of what's happening. - if (argv.help) { - // TODO: add target help - return throwError('Target help NYI.'); - // architect.help(targetOptions, logger); - } else { - const builderConfig = architect.getBuilderConfiguration(targetSpec); + if (!configFilePath) { + logger.fatal(`Workspace configuration file (${configFileNames.join(', ')}) cannot be found in ` + + `'${currentPath}' or in parent directories.`); - return architect.run(builderConfig, { logger }); - } - }), -).subscribe({ - next: (buildEvent => lastBuildEvent = buildEvent), - complete: () => process.exit(lastBuildEvent.success ? 0 : 1), - error: (err: Error) => { - logger.fatal(err.message); - if (err.stack) { - logger.fatal(err.stack); - } - process.exit(1); - }, -}); + return 3; + } + + const root = dirname(normalize(configFilePath)); + const configContent = readFileSync(configFilePath, 'utf-8'); + const workspaceJson = JSON.parse(configContent); + + const registry = new schema.CoreSchemaRegistry(); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); + + const host = new NodeJsSyncHost(); + const workspace = new experimental.workspace.Workspace(root, host); + + await workspace.loadWorkspaceFromJson(workspaceJson).toPromise(); + + return await _executeTarget(logger, workspace, root, argv, registry); +} + +main(process.argv.slice(2)) + .then(code => { + process.exit(code); + }, err => { + console.error('Error: ' + err.stack || err.message || err); + process.exit(-1); + }); diff --git a/packages/angular_devkit/architect_cli/package.json b/packages/angular_devkit/architect_cli/package.json index 183dee060281..c90812606347 100644 --- a/packages/angular_devkit/architect_cli/package.json +++ b/packages/angular_devkit/architect_cli/package.json @@ -12,10 +12,13 @@ "tooling" ], "dependencies": { - "@angular-devkit/core": "0.0.0", "@angular-devkit/architect": "0.0.0", + "@angular-devkit/core": "0.0.0", + "@types/progress": "^2.0.3", + "ascii-progress": "^1.0.5", "minimist": "1.2.0", - "symbol-observable": "1.2.0", - "rxjs": "6.3.3" + "progress": "^2.0.3", + "rxjs": "6.3.3", + "symbol-observable": "1.2.0" } } diff --git a/packages/angular_devkit/architect_cli/src/progress.ts b/packages/angular_devkit/architect_cli/src/progress.ts new file mode 100644 index 000000000000..d85763ceb56f --- /dev/null +++ b/packages/angular_devkit/architect_cli/src/progress.ts @@ -0,0 +1,98 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { terminal } from '@angular-devkit/core'; +import * as ProgressBar from 'progress'; +import * as readline from 'readline'; + +export class MultiProgressBar { + private _bars = new Map(); + + constructor(private _status: string, private _stream = process.stderr) {} + private _add(id: Key, data: T): { data: T, bar: ProgressBar } { + const width = Math.min(80, terminal.getCapabilities(this._stream).columns || 80); + const value = { + data, + bar: new ProgressBar(this._status, { + clear: true, + total: 1, + width: width, + complete: '#', + incomplete: '.', + stream: this._stream, + }), + }; + this._bars.set(id, value); + readline.moveCursor(this._stream, 0, 1); + + return value; + } + + complete(id: Key) { + const maybeBar = this._bars.get(id); + if (maybeBar) { + maybeBar.bar.complete = true; + } + } + + add(id: Key, data: T) { + this._add(id, data); + } + + get(key: Key): T | undefined { + const maybeValue = this._bars.get(key); + + return maybeValue && maybeValue.data; + } + has(key: Key) { + return this._bars.has(key); + } + update(key: Key, data: T, current?: number, total?: number) { + let maybeBar = this._bars.get(key); + + if (!maybeBar) { + maybeBar = this._add(key, data); + } + + maybeBar.data = data; + if (total !== undefined) { + maybeBar.bar.total = total; + } + if (current !== undefined) { + maybeBar.bar.curr = Math.max(0, Math.min(current, maybeBar.bar.total)); + } + } + + render(max = Infinity, sort?: (a: T, b: T) => number) { + const stream = this._stream; + + readline.moveCursor(stream, 0, -this._bars.size); + readline.cursorTo(stream, 0); + + let values: Iterable<{ data: T, bar: ProgressBar }> = this._bars.values(); + if (sort) { + values = [...values].sort((a, b) => sort(a.data, b.data)); + } + + for (const { data, bar } of values) { + if (max-- == 0) { + return; + } + + bar.render(data); + readline.moveCursor(stream, 0, 1); + readline.cursorTo(stream, 0); + } + } + + terminate() { + for (const { bar } of this._bars.values()) { + bar.terminate(); + } + this._bars.clear(); + } +} diff --git a/yarn.lock b/yarn.lock index fbb2d72aaa8a..94e9afde1add 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,6 +409,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-6.14.0.tgz#85c6998293fc6f2945915419296c7fbb63384f66" integrity sha512-6tQyh4Q4B5pECcXBOQDZ5KjyBIxRZGzrweGPM47sAYTdVG4+7R+2EGMTmp0h6ZwgqHrFRCeg2gdhsG9xXEl2Sg== +"@types/progress@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/progress/-/progress-2.0.3.tgz#7ccbd9c6d4d601319126c469e73b5bb90dfc8ccc" + integrity sha512-bPOsfCZ4tsTlKiBjBhKnM8jpY5nmIll166IPD58D92hR7G7kZDfx5iB9wGF4NfZrdKolebjeAr3GouYkSGoJ/A== + dependencies: + "@types/node" "*" + "@types/q@^0.0.32": version "0.0.32" resolved "http://registry.npmjs.org/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" @@ -951,6 +958,13 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi.js@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/ansi.js/-/ansi.js-0.0.5.tgz#e3e9e45eb6977ba0eeeeed11677d12144675348c" + integrity sha1-4+nkXraXe6Du7u0RZ30SFEZ1NIw= + dependencies: + on-new-line "0.0.1" + anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" @@ -1109,6 +1123,17 @@ asap@^2.0.0, asap@~2.0.3: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= +ascii-progress@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/ascii-progress/-/ascii-progress-1.0.5.tgz#9610aa127ab794af561e893613c36c906f78d9ee" + integrity sha1-lhCqEnq3lK9WHok2E8NskG942e4= + dependencies: + ansi.js "0.0.5" + end-with "^1.0.2" + get-cursor-position "1.0.3" + on-new-line "1.0.0" + start-with "^1.0.2" + ascli@~1: version "1.0.1" resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc" @@ -3244,6 +3269,11 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +end-with@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/end-with/-/end-with-1.0.2.tgz#a432755ab4f51e7fc74f3a719c6b81df5d668bdc" + integrity sha1-pDJ1WrT1Hn/HTzpxnGuB311mi9w= + engine.io-client@~3.1.0: version "3.1.6" resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.1.6.tgz#5bdeb130f8b94a50ac5cbeb72583e7a4a063ddfd" @@ -4118,6 +4148,11 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== +get-cursor-position@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-cursor-position/-/get-cursor-position-1.0.3.tgz#0e41d60343b705836a528d69a5e099e2c5108d63" + integrity sha1-DkHWA0O3BYNqUo1ppeCZ4sUQjWM= + get-pkg-repo@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" @@ -7139,6 +7174,16 @@ on-headers@~1.0.1: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" integrity sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c= +on-new-line@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/on-new-line/-/on-new-line-0.0.1.tgz#99339cb06dcfe3e78d6964a2ef2af374a145f8fb" + integrity sha1-mTOcsG3P4+eNaWSi7yrzdKFF+Ps= + +on-new-line@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/on-new-line/-/on-new-line-1.0.0.tgz#8585bc2866c8c0e192e410a6d63bdd1722148ae7" + integrity sha1-hYW8KGbIwOGS5BCm1jvdFyIUiuc= + once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -7785,7 +7830,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.1: +progress@^2.0.1, progress@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -9460,6 +9505,11 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= +start-with@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/start-with/-/start-with-1.0.2.tgz#a069a5f46a95fca7f0874f85a28f653d0095c267" + integrity sha1-oGml9GqV/Kfwh0+Foo9lPQCVwmc= + static-eval@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.0.tgz#0e821f8926847def7b4b50cda5d55c04a9b13864" From 0c549df673340e7e1c519306d54896729df56f32 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 14 Feb 2019 13:26:46 -0800 Subject: [PATCH 10/11] fix(@angular-devkit/core): fix true schemas post transform step Also tighten the types a bit, and add a test that failed before and works now. --- etc/api/angular_devkit/core/src/_golden-api.d.ts | 4 ++-- .../angular_devkit/core/src/json/schema/registry.ts | 8 -------- .../core/src/json/schema/registry_spec.ts | 12 ++++++++++++ .../core/src/json/schema/transforms.ts | 8 ++++++-- .../angular_devkit/core/src/json/schema/visitor.ts | 10 +++++++--- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/etc/api/angular_devkit/core/src/_golden-api.d.ts b/etc/api/angular_devkit/core/src/_golden-api.d.ts index e07a427eb66f..7dab611a0ced 100644 --- a/etc/api/angular_devkit/core/src/_golden-api.d.ts +++ b/etc/api/angular_devkit/core/src/_golden-api.d.ts @@ -5,7 +5,7 @@ export interface AdditionalPropertiesValidatorError extends SchemaValidatorError }; } -export declare function addUndefinedDefaults(value: JsonValue, _pointer: JsonPointer, schema?: JsonObject): JsonValue; +export declare function addUndefinedDefaults(value: JsonValue, _pointer: JsonPointer, schema?: JsonSchema): JsonValue; export declare class AliasHost extends ResolverHost { protected _aliases: Map; @@ -989,7 +989,7 @@ export declare class UnsupportedPlatformException extends BaseException { export declare type UriHandler = (uri: string) => Observable | Promise | null | undefined; -export declare function visitJson(json: JsonValue, visitor: JsonVisitor, schema?: JsonObject, refResolver?: ReferenceResolver, context?: ContextT): Observable; +export declare function visitJson(json: JsonValue, visitor: JsonVisitor, schema?: JsonSchema, refResolver?: ReferenceResolver, context?: ContextT): Observable; export declare function visitJsonSchema(schema: JsonSchema, visitor: JsonSchemaVisitor): void; diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index aa98bac3e18c..b04bbc8c7b82 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -347,10 +347,6 @@ export class CoreSchemaRegistry implements SchemaRegistry { // tslint:disable-next-line:no-any https://github.com/ReactiveX/rxjs/issues/3989 result = (result as any).pipe( ...[...this._pre].map(visitor => concatMap((data: JsonValue) => { - if (schema === false || schema === true) { - return of(data); - } - return visitJson(data, visitor, schema, this._resolver, validate); })), ); @@ -418,10 +414,6 @@ export class CoreSchemaRegistry implements SchemaRegistry { // tslint:disable-next-line:no-any https://github.com/ReactiveX/rxjs/issues/3989 result = (result as any).pipe( ...[...this._post].map(visitor => concatMap((data: JsonValue) => { - if (schema === false || schema === true) { - return of(schema); - } - return visitJson(data, visitor, schema, this._resolver, validate); })), ); diff --git a/packages/angular_devkit/core/src/json/schema/registry_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts index fa512977605f..337e82e719df 100644 --- a/packages/angular_devkit/core/src/json/schema/registry_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -382,6 +382,18 @@ describe('CoreSchemaRegistry', () => { .toPromise().then(done, done.fail); }); + it('works with true as a schema and post-transforms', async () => { + const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); + const data: any = { a: 1, b: 2 }; // tslint:disable-line:no-any + + const validate = await registry.compile(true).toPromise(); + const result = await validate(data).toPromise(); + + expect(result.success).toBe(true); + expect(result.data).toBe(data); + }); + it('adds undefined properties', done => { const registry = new CoreSchemaRegistry(); registry.addPostTransform(addUndefinedDefaults); diff --git a/packages/angular_devkit/core/src/json/schema/transforms.ts b/packages/angular_devkit/core/src/json/schema/transforms.ts index 9c6694e4e32e..e047d3c38f2a 100644 --- a/packages/angular_devkit/core/src/json/schema/transforms.ts +++ b/packages/angular_devkit/core/src/json/schema/transforms.ts @@ -7,14 +7,18 @@ */ import { JsonObject, JsonValue, isJsonObject } from '../interface'; import { JsonPointer } from './interface'; +import { JsonSchema } from './schema'; import { getTypesOfSchema } from './utility'; export function addUndefinedDefaults( value: JsonValue, _pointer: JsonPointer, - schema?: JsonObject, + schema?: JsonSchema, ): JsonValue { - if (!schema) { + if (schema === true || schema === false) { + return value; + } + if (schema === undefined) { return value; } diff --git a/packages/angular_devkit/core/src/json/schema/visitor.ts b/packages/angular_devkit/core/src/json/schema/visitor.ts index 130ef4925846..b3b46c40df9f 100644 --- a/packages/angular_devkit/core/src/json/schema/visitor.ts +++ b/packages/angular_devkit/core/src/json/schema/visitor.ts @@ -19,7 +19,7 @@ export interface ReferenceResolver { } function _getObjectSubSchema( - schema: JsonObject | undefined, + schema: JsonSchema | undefined, key: string, ): JsonObject | undefined { if (typeof schema !== 'object' || schema === null) { @@ -51,11 +51,15 @@ function _visitJsonRecursive( json: JsonValue, visitor: JsonVisitor, ptr: JsonPointer, - schema?: JsonObject, + schema?: JsonSchema, refResolver?: ReferenceResolver, context?: ContextT, // tslint:disable-line:no-any root?: JsonObject | JsonArray, ): Observable { + if (schema === true || schema === false) { + // There's no schema definition, so just visit the JSON recursively. + schema = undefined; + } if (schema && schema.hasOwnProperty('$ref') && typeof schema['$ref'] == 'string') { if (refResolver) { const resolved = refResolver(schema['$ref'] as string, context); @@ -132,7 +136,7 @@ function _visitJsonRecursive( export function visitJson( json: JsonValue, visitor: JsonVisitor, - schema?: JsonObject, + schema?: JsonSchema, refResolver?: ReferenceResolver, context?: ContextT, // tslint:disable-line:no-any ): Observable { From 1b4a67f63f17f47d8a8fff4079d103264782ce15 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 14 Feb 2019 13:29:39 -0800 Subject: [PATCH 11/11] feat(@angular-devkit/core): remove Log messages from Job API If a system wants to have logging it should multiplex it itself on a channel. Also changed the previous Architect commits to remove usage of Logs and move to a "log" channel. --- .../architect/src/create-builder.ts | 15 +++++++++++++-- .../architect/src/schedule-by-name.ts | 11 ++++++----- .../core/src/experimental/jobs/README.md | 8 +------- .../core/src/experimental/jobs/api.ts | 16 ---------------- .../core/src/experimental/jobs/architecture.md | 4 +--- .../src/experimental/jobs/create-job-handler.ts | 15 +-------------- .../src/experimental/jobs/simple-scheduler.ts | 10 +--------- 7 files changed, 23 insertions(+), 56 deletions(-) diff --git a/packages/angular_devkit/architect/src/create-builder.ts b/packages/angular_devkit/architect/src/create-builder.ts index 56053ad81a27..9ff2ff6824b8 100644 --- a/packages/angular_devkit/architect/src/create-builder.ts +++ b/packages/angular_devkit/architect/src/create-builder.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { experimental, isPromise, json } from '@angular-devkit/core'; +import { experimental, isPromise, json, logging } from '@angular-devkit/core'; import { Observable, Subscription, from, isObservable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; import { @@ -17,6 +17,7 @@ import { BuilderProgressState, Target, TypedBuilderProgress, + targetStringFromTarget, } from './api'; import { Builder, BuilderSymbol, BuilderVersionSymbol } from './internal'; import { scheduleByName, scheduleByTarget } from './schedule-by-name'; @@ -28,13 +29,16 @@ export function createBuilder( const cjh = experimental.jobs.createJobHandler; const handler = cjh((options, context) => { const scheduler = context.scheduler; - const logger = context.logger; const progressChannel = context.createChannel('progress'); + const logChannel = context.createChannel('log'); let currentState: BuilderProgressState = BuilderProgressState.Stopped; let current = 0; let status = ''; let total = 1; + function log(entry: logging.LogEntry) { + logChannel.next(entry); + } function progress(progress: TypedBuilderProgress, context: BuilderContext) { currentState = progress.state; if (progress.state === BuilderProgressState.Running) { @@ -74,6 +78,13 @@ export function createBuilder( function onInput(i: BuilderInput) { const builder = i.info as BuilderInfo; + const loggerName = i.target + ? targetStringFromTarget(i.target as Target) + : builder.builderName; + const logger = new logging.Logger(loggerName); + + subscriptions.push(logger.subscribe(entry => log(entry))); + const context: BuilderContext = { builder, workspaceRoot: i.workspaceRoot, diff --git a/packages/angular_devkit/architect/src/schedule-by-name.ts b/packages/angular_devkit/architect/src/schedule-by-name.ts index d3bc12b07040..0022c9b53207 100644 --- a/packages/angular_devkit/architect/src/schedule-by-name.ts +++ b/packages/angular_devkit/architect/src/schedule-by-name.ts @@ -65,15 +65,16 @@ export async function scheduleByName( job.input.next(message); } + const logChannelSub = job.getChannel('log').subscribe(entry => { + logger.next(entry); + }); + const s = job.outboundBus.subscribe( - message => { - if (message.kind == experimental.jobs.JobOutboundMessageKind.Log) { - logger.log(message.entry.level, message.entry.message, message.entry); - } - }, + undefined, undefined, () => { s.unsubscribe(); + logChannelSub.unsubscribe(); if (stateSubscription) { stateSubscription.unsubscribe(); } diff --git a/packages/angular_devkit/core/src/experimental/jobs/README.md b/packages/angular_devkit/core/src/experimental/jobs/README.md index 3989c30d0d38..e1640a5382b9 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/README.md +++ b/packages/angular_devkit/core/src/experimental/jobs/README.md @@ -19,10 +19,6 @@ The I/O model is like that of an executable, where the argument corresponds to a command line, the input channel to STDIN, the output channel to STDOUT, and the channels would be additional output streams. -In addition, a `Job` has a logging channel that can be used to log messages to the user. The -code that schedules the job must listen for or forward these messages. You can think of those -messages as STDERR. - ## LifeCycle A `Job` goes through multiple LifeCycle messages before its completion; 1. `JobState.Queued`. The job was queued and is waiting. This is the default state from the @@ -162,8 +158,7 @@ export const add = jobs.createJobHandler( ``` You can also return a Promise or an Observable, as jobs are asynchronous. This helper will set -start and end messages appropriately, and will pass in a logger. It will also manage channels -automatically (see below). +start and end messages appropriately. It will also manage channels automatically (see below). A more complex job can be declared like this: @@ -246,7 +241,6 @@ member of the `JobOutboundMessage` interface dictates what kind of message it dependent jobs. `createJobHandler()` automatically send this message. 1. `JobOutboundMessageKind.Pong`. The job should answer a `JobInboundMessageKind.Ping` message with this. Automatically done by `createJobHandler()`. -1. `JobOutboundMessageKind.Log`. A logging message (side effect that should be shown to the user). 1. `JobOutboundMessageKind.Output`. An `Output` has been generated by the job. 1. `JobOutboundMessageKind.ChannelMessage`, `JobOutboundMessageKind.ChannelError` and `JobOutboundMessageKind.ChannelComplete` are used for output channels. These correspond to the diff --git a/packages/angular_devkit/core/src/experimental/jobs/api.ts b/packages/angular_devkit/core/src/experimental/jobs/api.ts index 227c7e2eb51d..5d109bf0904b 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/api.ts +++ b/packages/angular_devkit/core/src/experimental/jobs/api.ts @@ -7,7 +7,6 @@ */ import { Observable, Observer } from 'rxjs'; import { JsonObject, JsonValue, schema } from '../../json/index'; -import { LogEntry, LoggerApi } from '../../logger/index'; import { DeepReadonly } from '../../utils/index'; /** @@ -135,7 +134,6 @@ export enum JobOutboundMessageKind { Pong = 'p', // Feedback messages. - Log = 'l', Output = 'o', // Channel specific messages. @@ -172,14 +170,6 @@ export interface JobOutboundMessageStart extends JobOutboundMessageBase { readonly kind: JobOutboundMessageKind.Start; } -/** - * A logging message, supporting the logging.LogEntry. - */ -export interface JobOutboundMessageLog extends JobOutboundMessageBase { - readonly kind: JobOutboundMessageKind.Log; - readonly entry: LogEntry; -} - /** * An output value is available. */ @@ -274,7 +264,6 @@ export interface JobOutboundMessagePong extends JobOutboundMessageBase { export type JobOutboundMessage = JobOutboundMessageOnReady | JobOutboundMessageStart - | JobOutboundMessageLog | JobOutboundMessageOutput | JobOutboundMessageChannelCreate | JobOutboundMessageChannelMessage @@ -381,11 +370,6 @@ export interface Job< * Options for scheduling jobs. */ export interface ScheduleJobOptions { - /** - * Where should logging be passed in. By default logging will be dropped. - */ - logger?: LoggerApi; - /** * Jobs that need to finish before scheduling this job. These dependencies will be passed * to the job itself in its context. diff --git a/packages/angular_devkit/core/src/experimental/jobs/architecture.md b/packages/angular_devkit/core/src/experimental/jobs/architecture.md index 216339f167dd..ded5a0bd5f83 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/architecture.md +++ b/packages/angular_devkit/core/src/experimental/jobs/architecture.md @@ -101,7 +101,6 @@ and output values (STDOUT) as well as diagnostic (STDERR). They can be plugged o ,______________________ JobInboundMessage --> | handler(argument: A) | --> JobOutboundMessage - `⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻⎻ - JobOutboundMessageKind.Log - JobOutboundMessageKind.Output - ... ``` @@ -129,7 +128,6 @@ and output values (STDOUT) as well as diagnostic (STDERR). They can be plugged o unblock dependent jobs. `createJobHandler()` automatically send this message. 1. `JobOutboundMessageKind.Pong`. The job should answer a `JobInboundMessageKind.Ping` message with this. Automatically done by `createJobHandler()`. -1. `JobOutboundMessageKind.Log`. A logging message (side effect that should be shown to the user). 1. `JobOutboundMessageKind.Output`. An `Output` has been generated by the job. 1. `JobOutboundMessageKind.ChannelMessage`, `JobOutboundMessageKind.ChannelError` and `JobOutboundMessageKind.ChannelComplete` are used for output channels. These correspond to @@ -251,7 +249,7 @@ example would be an operator that takes a module path and run the job from that process. Or even a separate server, using HTTP calls. Another limitation is that the boilerplate is complex. Manually managing start/end life cycle, and -other messages such as logging, etc. is tedious and requires a lot of code. A good way to keep +other messages such as ping/pong, etc. is tedious and requires a lot of code. A good way to keep this limitation under control is to provide helpers to create `JobHandler`s which manage those messages for the developer. A simple handler could be to get a `Promise` and return the output of that promise automatically. diff --git a/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts b/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts index 40b63c94bd05..08f381dc081f 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts +++ b/packages/angular_devkit/core/src/experimental/jobs/create-job-handler.ts @@ -10,7 +10,7 @@ import { Observable, Observer, Subject, Subscription, from, isObservable, of } f import { switchMap, tap } from 'rxjs/operators'; import { BaseException } from '../../exception/index'; import { JsonValue } from '../../json/index'; -import { Logger, LoggerApi } from '../../logger/index'; +import { LoggerApi } from '../../logger'; import { isPromise } from '../../utils/index'; import { JobDescription, @@ -37,7 +37,6 @@ export interface SimpleJobHandlerContext< I extends JsonValue, O extends JsonValue, > extends JobHandlerContext { - logger: LoggerApi; createChannel: (name: string) => Observer; input: Observable; } @@ -100,23 +99,12 @@ export function createJobHandler { - subject.next({ - kind: JobOutboundMessageKind.Log, - description, - entry, - }); - }); - // Execute the function with the additional context. const channels = new Map>(); const newContext: SimpleJobHandlerContext = { ...context, input: inputChannel.asObservable(), - logger, createChannel(name: string) { if (channels.has(name)) { throw new ChannelAlreadyExistException(name); @@ -164,7 +152,6 @@ export function createJobHandler complete(), ); subscription.add(inboundSub); - subscription.add(logSub); return subscription; }); diff --git a/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts b/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts index 76ade7049bb1..9c1c4fb7d88a 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts +++ b/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts @@ -22,13 +22,12 @@ import { filter, first, ignoreElements, - map, share, + map, shareReplay, switchMap, tap, } from 'rxjs/operators'; import { JsonValue, schema } from '../../json'; -import { NullLogger } from '../../logger'; import { Job, JobDescription, @@ -305,8 +304,6 @@ export class SimpleScheduler< let state = JobState.Queued; let pingId = 0; - const logger = options.logger ? options.logger : new NullLogger(); - // Create the input channel by having a filter. const input = new Subject(); input.pipe( @@ -348,11 +345,6 @@ export class SimpleScheduler< state = this._updateState(message, state); switch (message.kind) { - case JobOutboundMessageKind.Log: - const entry = message.entry; - logger.log(entry.level, entry.message, entry); - break; - case JobOutboundMessageKind.ChannelCreate: { const maybeSubject = channelsSubject.get(message.name); // If it doesn't exist or it's closed on the other end.