diff --git a/packages/angular_devkit/architect/builders/and.ts b/packages/angular_devkit/architect/builders/and.ts index d7190a13da91..587ad9b3e445 100644 --- a/packages/angular_devkit/architect/builders/and.ts +++ b/packages/angular_devkit/architect/builders/and.ts @@ -5,11 +5,11 @@ * 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 { of, from, EMPTY } from "rxjs"; -import { concatMap, map, mergeMap } from "rxjs/operators"; -import { Schema as OperatorSchema } from './operator-schema'; +import { json } from '@angular-devkit/core'; +import { EMPTY, from, of } from 'rxjs'; +import { concatMap, map, mergeMap } from 'rxjs/operators'; import { BuilderOutput, BuilderRun, createBuilder } from '../src/api'; +import { Schema as OperatorSchema } from './operator-schema'; export default createBuilder((options, context) => { let allRuns: Promise<[number, BuilderRun]>[] | null = null; @@ -17,7 +17,8 @@ export default createBuilder((options, context if (options.targets) { allRuns = options.targets.map((targetStr, i) => { const [project, target, configuration] = targetStr.split(/:/g, 3); - return context.scheduleTarget(project, target, configuration, {}) + + return context.scheduleTarget({ project, target, configuration }, {}) .then(run => [i, run] as [number, BuilderRun]); }); } else if (options.builders && options.options) { diff --git a/packages/angular_devkit/architect/node/node-modules-architect-host.ts b/packages/angular_devkit/architect/node/node-modules-architect-host.ts index 39a161dd7e59..803d7d0a2713 100644 --- a/packages/angular_devkit/architect/node/node-modules-architect-host.ts +++ b/packages/angular_devkit/architect/node/node-modules-architect-host.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 } from '@angular-devkit/core'; +import { experimental, json } from '@angular-devkit/core'; import { resolve } from '@angular-devkit/core/node'; import * as path from 'path'; import { ArchitectHost, BuilderInfo } from '../src/api'; @@ -80,4 +80,16 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost { getWorkspaceRoot(): string { return this._root; } + + 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), + }; + } } diff --git a/packages/angular_devkit/architect/src/api.ts b/packages/angular_devkit/architect/src/api.ts index 961d9a277471..1903b92e5a83 100644 --- a/packages/angular_devkit/architect/src/api.ts +++ b/packages/angular_devkit/architect/src/api.ts @@ -5,13 +5,13 @@ * 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, logging, normalize } from '@angular-devkit/core'; +import { experimental, isPromise, json, logging } from '@angular-devkit/core'; import { Observable, from, isObservable, of } from 'rxjs'; -import { concatMap, first, map, switchMap, tap } from 'rxjs/operators'; +import { concatMap, first, map, shareReplay, switchMap, tap } from 'rxjs/operators'; import { JobInboundMessageKind, JobOutboundMessageKind } from '../../core/src/experimental/jobs'; import { Schema as RealBuilderInput, Target } from './input-schema'; import { Schema as RealBuilderOutput } from './output-schema'; - +import { Schema as RealBuilderProgress } from './progress-schema'; export type Target = Target; @@ -29,45 +29,41 @@ export type BuilderRegistry< export type BuilderInput = json.JsonObject & RealBuilderInput; export type BuilderOutput = json.JsonObject & RealBuilderOutput; - +export type BuilderProgress = json.JsonObject & RealBuilderProgress; const inputSchema = require('./input-schema.json'); const outputSchema = require('./output-schema.json'); +const progressSchema = require('./progress-schema.json'); - -export enum BuilderState { - Stopped = 'stopped', - Waiting = 'waiting', - Building = 'building', - Error = 'error', -} - export interface BuilderRun { builderName: string; // This always replay the last output. output: Observable; - // This always replay the last state. - state: Observable; - - // + // This will always replay the last progress on new subscriptions. + progress: Observable; } export interface BuilderDescription extends experimental.jobs.JobDescription { info: BuilderInfo; } +export const BuilderSymbol = Symbol.for('@angular-devkit/architect:builder'); +export const VersionSymbol = Symbol.for('@angular-devkit/architect:version'); + + export interface Builder { handler: experimental.jobs.JobHandler; - '@@Architect_Builder': true; + [BuilderSymbol]: true; + [VersionSymbol]: string; } export interface BuilderContext { logger: logging.LoggerApi; - projectRoot: string; + workspaceRoot: string; currentDirectory: string; // Target is optional if a builder was ran by name. @@ -75,10 +71,8 @@ export interface BuilderContext { // scheduleJob() scheduleTarget( - project: string, - target: string, - configuration: string | undefined, - options: json.JsonObject, + target: Target, + overrides: json.JsonObject, ): Promise; // scheduleBuilder() @@ -126,6 +120,62 @@ export interface ArchitectHost { getCurrentDirectory(): string; getWorkspaceRoot(): string; + + getOptionsForTarget(target: Target): Promise; +} + +/** + * Returns a string of "project:target[:configuration]" for the target object. + */ +export function targetToTargetString({project, target, configuration}: Target) { + return `${project}:${target}${configuration !== undefined ? ':' + configuration : ''}`; +} + +async function _scheduleTarget( + target: Target, + overrides: json.JsonObject, + scheduler: experimental.jobs.Scheduler, + options: { + logger: logging.Logger, + workspaceRoot: string, + currentDirectory: string, + }, +): Promise { + const name = `{${targetToTargetString(target)}}`; + const job = scheduler.schedule<{}, BuilderInput, BuilderOutput>(name, {}); + + // Wait for the job to be ready. + if (job.state !== experimental.jobs.JobState.Started) { + job.outboundBus.subscribe(event => { + if (event.kind === experimental.jobs.JobOutboundMessageKind.Start) { + job.input.next({ + currentDirectory: options.workspaceRoot, + workspaceRoot: options.currentDirectory, + reason: 'initial', + options: overrides, + target, + } as BuilderInput); + } + }); + } else { + + } + + job.outboundBus.subscribe( + message => { + if (message.kind == experimental.jobs.JobOutboundMessageKind.Log) { + options.logger.next(message.entry); + } + }, + ); + + const description = await job.description.toPromise(); + + return { + builderName: description.name, + output: job.output.pipe(shareReplay(1)), + progress: new Observable(), + }; } export function createBuilder( @@ -136,11 +186,7 @@ export function createBuilder( context: experimental.jobs.JobHandlerContext, ) { const description = context.description; - - function scheduleTarget(project: string, target: string, configuration: string | undefined, options: json.JsonObject): Promise { - const name = `{${project}:${target}${configuration ? ':' + configuration : ''}}`; - const job = context.scheduler.schedule(name, options); - } + const scheduler = context.scheduler; return new Observable>(observer => { const logger = new logging.Logger('job'); @@ -156,11 +202,17 @@ export function createBuilder( if (x.kind === experimental.jobs.JobInboundMessageKind.Input) { const i = x.value; const context: BuilderContext = { - projectRoot: normalize(i.workspaceRoot), - currentDirectory: normalize(i.currentDirectory), + workspaceRoot: i.workspaceRoot, + currentDirectory: i.currentDirectory, target: i.target, logger, - scheduleTarget, + scheduleTarget(target: Target, overrides: json.JsonObject) { + return _scheduleTarget(target, overrides, scheduler, { + logger, + workspaceRoot: i.workspaceRoot, + currentDirectory: i.currentDirectory, + }); + }, }; let result = fn(i.options as OptT, context); @@ -193,7 +245,8 @@ export function createBuilder( return { handler: Object.assign(handler, { jobDescription: {} }), - '@@Architect_Builder': true, + [BuilderSymbol]: true, + [VersionSymbol]: require('../package.json').version, }; } @@ -201,12 +254,18 @@ function _createBackwardCompatibleJobHandlerFromBuilderInfo( info: BuilderInfo, target: Target | undefined, registry: json.schema.SchemaRegistry, + baseOptions: json.JsonObject, ): Observable { // This will only work in Node, but it's not illegal in a browser (as long as it's not // executed). return from(import('./backward-compatible')).pipe( switchMap(module => { - return from(module.createBackwardCompatibleJobHandlerFromBuilderInfo(info, target, registry)); + return from(module.createBackwardCompatibleJobHandlerFromBuilderInfo( + info, + target, + registry, + baseOptions, + )); }), ); } @@ -215,6 +274,7 @@ function _createJobHandlerFromBuilderInfo( info: BuilderInfo, _target: Target | undefined, registry: json.schema.SchemaRegistry, + baseOptions: json.JsonObject, ): Observable { const jobDescription: BuilderDescription = { name: info.name, @@ -226,7 +286,7 @@ function _createJobHandlerFromBuilderInfo( const loader = async () => { const builder = (await import(info.import)).default; - if (builder['@@Architect_Builder']) { + if (builder[BuilderSymbol]) { return builder.handler; } @@ -238,7 +298,10 @@ function _createJobHandlerFromBuilderInfo( concatMap(message => { if (message.kind === JobInboundMessageKind.Input) { const v = message.value as BuilderInput; - const options = v.options || {}; + const options = { + ...baseOptions, + ...v.options, + }; // Validate v.options return registry.compile(info.optionSchema).pipe( @@ -298,7 +361,7 @@ export class ArchitectBuilderJobRegistry implements BuilderRegistry { return from(this._host.resolveBuilder(name)); } - protected _createBuilder(info: BuilderInfo, target?: Target) { + protected _createBuilder(info: BuilderInfo, target?: Target, options?: json.JsonObject) { const cache = this._cache; if (cache && cache.has(info.name)) { return of(cache.get(info.name) || null); @@ -306,9 +369,13 @@ export class ArchitectBuilderJobRegistry implements BuilderRegistry { let result: Observable; if (info.version == 1) { - result = _createBackwardCompatibleJobHandlerFromBuilderInfo(info, target, this._registry); + result = _createBackwardCompatibleJobHandlerFromBuilderInfo( + info, + target, + this._registry, + options || {}); } else { - result = _createJobHandlerFromBuilderInfo(info, target, this._registry); + result = _createJobHandlerFromBuilderInfo(info, target, this._registry, options || {}); } if (cache) { @@ -352,9 +419,21 @@ export class ArchitectTargetJobRegistry extends ArchitectBuilderJobRegistry { configuration: m[3], }; - return from(this._host.getBuilderNameForTarget(target)).pipe( - concatMap(builderStr => this._resolveBuilder(builderStr)), - concatMap(builderInfo => builderInfo ? this._createBuilder(builderInfo, target) : of(null)), + return from(Promise.all([ + this._host.getBuilderNameForTarget(target), + this._host.getOptionsForTarget(target), + ])).pipe( + concatMap(([builderStr, options]) => { + 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>; } @@ -392,32 +471,17 @@ export class Architect { return this._scheduler.has(name); } - scheduleTarget( - project: string, - target: string, - configuration: string | undefined, - options: A, - ): experimental.jobs.Job<{}, BuilderInput, BuilderOutput> { - const name = `{${project}:${target}${configuration === undefined ? '' : ':' + configuration}}`; - const job = this._scheduler.schedule(name, {}); - - // Wait for the job to be ready. - job.outboundBus.subscribe(event => { - if (event.kind === experimental.jobs.JobOutboundMessageKind.Start) { - job.input.next({ - currentDirectory: this._host.getCurrentDirectory(), - workspaceRoot: this._host.getWorkspaceRoot(), - reason: 'initial', - options, - target: { - project, - target, - configuration, - }, - } as BuilderInput); - } + scheduleTarget( + target: Target, + overrides: json.JsonObject, + options: { + logger?: logging.Logger, + } = {}, + ): Promise { + return _scheduleTarget(target, overrides, this._scheduler, { + logger: options.logger || new logging.NullLogger(), + currentDirectory: this._host.getCurrentDirectory(), + workspaceRoot: this._host.getWorkspaceRoot(), }); - - return job; } } diff --git a/packages/angular_devkit/architect/src/backward-compatible.ts b/packages/angular_devkit/architect/src/backward-compatible.ts index 638bd663bc7d..ee5112fd75a9 100644 --- a/packages/angular_devkit/architect/src/backward-compatible.ts +++ b/packages/angular_devkit/architect/src/backward-compatible.ts @@ -252,6 +252,7 @@ export async function createBackwardCompatibleJobHandlerFromBuilderInfo( info: BuilderInfo, target: Target | undefined, registry: json.schema.SchemaRegistry, + baseOptions: json.JsonObject, ): Promise { const jobDescription: BuilderDescription = { name: info.name, @@ -278,7 +279,10 @@ export async function createBackwardCompatibleJobHandlerFromBuilderInfo( buildInput: BuilderInput, ): Promise<[legacy.Builder, json.JsonObject]> { const root = normalize(buildInput.workspaceRoot); - const options = buildInput.options; + const options = { + ...baseOptions, + ...buildInput.options, + }; const validate = await registry.compile(info.optionSchema).toPromise(); const result = await validate(options as json.JsonObject).toPromise(); 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..8435b8b48d8c --- /dev/null +++ b/packages/angular_devkit/architect/src/progress-schema.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "BuilderProgressSchema", + "title": "Progress schema for builders.", + "type": "object", + "properties": { + "current": { + "type": "number", + "minimum": 0 + }, + "total": { + "type": "number", + "minimum": 0 + }, + "state": { + "type": "string", + "enum": [ + "stopped", + "waiting", + "running", + "error" + ] + }, + "status": { + "type": "string" + }, + "error": true + }, + "required": [ + "current", + "state" + ] +} diff --git a/packages/angular_devkit/architect_cli/bin/architect.ts b/packages/angular_devkit/architect_cli/bin/architect.ts index 91da168ef7d6..90bd47dd1e66 100644 --- a/packages/angular_devkit/architect_cli/bin/architect.ts +++ b/packages/angular_devkit/architect_cli/bin/architect.ts @@ -83,47 +83,24 @@ async function _executeTarget( // Split a target into its parts. const targetStr = argv._.shift() || ''; - const [projectName, targetName, configurationName] = targetStr.split(':'); - - // Find the builder. - const maybeProjectTargets = workspace.getProjectTargets(projectName); - if (!maybeProjectTargets) { - logger.fatal(`Project ${JSON.stringify(projectName)} does not exist.`); - - return 4; - } - - const maybeTarget = maybeProjectTargets[targetName]; - if (!maybeTarget) { - logger.fatal(tags.oneLine` - Target ${JSON.stringify(targetName)} does not exist in project - ${JSON.stringify(projectName)}. - `); - - return 5; - } + const [project, target, configuration] = targetStr.split(':'); + const targetSpec = { project, target, configuration }; delete argv['help']; delete argv['_']; - const job = architect.scheduleTarget(projectName, targetName, configurationName, { - ...maybeTarget.options, - ...(configurationName && maybeTarget.configurations[configurationName] || 0), - ...argv, + + const run = await architect.scheduleTarget(targetSpec, argv, { + logger, }); - job.outboundBus.subscribe( - message => { - if (message.kind == experimental.jobs.JobOutboundMessageKind.Log) { - logger.next(message.entry); - } - }, - ); - job.output.subscribe( - o => console.log('next', o), + + run.output.subscribe( + o => console.log('output: ', JSON.stringify(o)), err => console.error('err', err), - () => console.log('complete'), + () => console.log('complete\n\n'), ); - await job.output.toPromise(); + // Wait for full completion of the builder. + await run.output.toPromise(); return 0; } diff --git a/packages/angular_devkit/build_angular/src/tslint/index.ts b/packages/angular_devkit/build_angular/src/tslint/index.ts index 69417b9fff9a..72b4b5778659 100644 --- a/packages/angular_devkit/build_angular/src/tslint/index.ts +++ b/packages/angular_devkit/build_angular/src/tslint/index.ts @@ -38,7 +38,7 @@ async function _loadTslint() { async function _run(config: TslintBuilderOptions, context: BuilderContext): Promise { - const systemRoot = context.projectRoot; + const systemRoot = context.workspaceRoot; const options = config; const projectName = context.target && context.target.project || ''; 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 bb8e380831ae..6660df15817c 100644 --- a/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts +++ b/packages/angular_devkit/core/src/experimental/jobs/simple-scheduler.ts @@ -151,7 +151,7 @@ export class SimpleScheduler< const description: JobDescription = { // Make a copy of it to be sure it's proper JSON. ...JSON.parse(JSON.stringify(handler.jobDescription)), - name, + name: handler.jobDescription.name || name, argument: handler.jobDescription.argument || true, input: handler.jobDescription.input || true, output: handler.jobDescription.output || true,