Skip to content

Commit d1ed5d4

Browse files
filipesilvahansl
authored andcommitted
feat(@angular-devkit/architect): expose builder schema
1 parent 3f2845a commit d1ed5d4

File tree

2 files changed

+118
-45
lines changed

2 files changed

+118
-45
lines changed

packages/angular_devkit/architect/src/architect.ts

Lines changed: 108 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,18 @@ import {
2323
} from '@angular-devkit/core';
2424
import { resolve as nodeResolve } from '@angular-devkit/core/node';
2525
import { Observable } from 'rxjs/Observable';
26+
import { forkJoin } from 'rxjs/observable/forkJoin';
2627
import { of } from 'rxjs/observable/of';
2728
import { _throw } from 'rxjs/observable/throw';
28-
import { concatMap } from 'rxjs/operators';
29+
import { concatMap, map } from 'rxjs/operators';
2930
import {
3031
BuildEvent,
3132
Builder,
3233
BuilderConstructor,
3334
BuilderContext,
3435
BuilderDescription,
35-
BuilderMap,
36+
BuilderPaths,
37+
BuilderPathsMap,
3638
} from './builder';
3739
import { Workspace } from './workspace';
3840

@@ -74,6 +76,12 @@ export class WorkspaceNotYetLoadedException extends BaseException {
7476
constructor() { super(`Workspace needs to be loaded before Architect is used.`); }
7577
}
7678

79+
export class BuilderNotFoundException extends BaseException {
80+
constructor(builder: string) {
81+
super(`Builder ${builder} could not be found.`);
82+
}
83+
}
84+
7785
export interface Target<OptionsT = {}> {
7886
root: Path;
7987
projectType: string;
@@ -87,26 +95,29 @@ export interface TargetOptions<OptionsT = {}> {
8795
configuration?: string;
8896
overrides?: Partial<OptionsT>;
8997
}
90-
9198
export class Architect {
92-
private readonly _workspaceSchema = join(normalize(__dirname), 'workspace-schema.json');
93-
private readonly _buildersSchema = join(normalize(__dirname), 'builders-schema.json');
99+
private readonly _workspaceSchemaPath = join(normalize(__dirname), 'workspace-schema.json');
100+
private readonly _buildersSchemaPath = join(normalize(__dirname), 'builders-schema.json');
101+
private _workspaceSchema: JsonObject;
102+
private _buildersSchema: JsonObject;
103+
private _architectSchemasLoaded = false;
104+
private _builderPathsMap = new Map<string, BuilderPaths>();
105+
private _builderDescriptionMap = new Map<string, BuilderDescription>();
106+
private _builderConstructorMap = new Map<string, BuilderConstructor<{}>>();
94107
private _workspace: Workspace;
95108

96109
constructor(private _root: Path, private _host: virtualFs.Host<{}>) { }
97110

98111
loadWorkspaceFromHost(workspacePath: Path) {
99-
return this._host.read(join(this._root, workspacePath)).pipe(
100-
concatMap((buffer) => {
101-
const json = JSON.parse(virtualFs.fileBufferToString(buffer));
102-
103-
return this.loadWorkspaceFromJson(json);
104-
}),
112+
return this._loadArchitectSchemas().pipe(
113+
concatMap(() => this._loadJsonFile(join(this._root, workspacePath))),
114+
concatMap(json => this.loadWorkspaceFromJson(json as {} as Workspace)),
105115
);
106116
}
107117

108118
loadWorkspaceFromJson(json: Workspace) {
109-
return this._validateAgainstSchema(json, this._workspaceSchema).pipe(
119+
return this._loadArchitectSchemas().pipe(
120+
concatMap(() => this._validateAgainstSchema(json, this._workspaceSchema)),
110121
concatMap((validatedWorkspace: Workspace) => {
111122
this._workspace = validatedWorkspace;
112123

@@ -115,6 +126,24 @@ export class Architect {
115126
);
116127
}
117128

129+
private _loadArchitectSchemas() {
130+
if (this._architectSchemasLoaded) {
131+
return of(null);
132+
} else {
133+
return forkJoin(
134+
this._loadJsonFile(this._workspaceSchemaPath),
135+
this._loadJsonFile(this._buildersSchemaPath),
136+
).pipe(
137+
concatMap(([workspaceSchema, buildersSchema]) => {
138+
this._workspaceSchema = workspaceSchema;
139+
this._buildersSchema = buildersSchema;
140+
141+
return of(null);
142+
}),
143+
);
144+
}
145+
}
146+
118147
getTarget<OptionsT>(options: TargetOptions = {}): Target<OptionsT> {
119148
let { project, target: targetName } = options;
120149
const { configuration, overrides } = options;
@@ -187,23 +216,27 @@ export class Architect {
187216

188217
return this.validateBuilderOptions(target, builderDescription);
189218
}),
190-
concatMap(() => of(this.getBuilder(builderDescription, context))),
219+
map(() => this.getBuilder(builderDescription, context)),
191220
concatMap(builder => builder.run(target)),
192221
);
193222
}
194223

195224
getBuilderDescription<OptionsT>(target: Target<OptionsT>): Observable<BuilderDescription> {
225+
// Check cache for this builder description.
226+
if (this._builderDescriptionMap.has(target.builder)) {
227+
return of(this._builderDescriptionMap.get(target.builder) as BuilderDescription);
228+
}
229+
196230
return new Observable((obs) => {
197231
// TODO: this probably needs to be more like NodeModulesEngineHost.
198232
const basedir = getSystemPath(this._root);
199233
const [pkg, builderName] = target.builder.split(':');
200234
const pkgJsonPath = nodeResolve(pkg, { basedir, resolvePackageJson: true });
201235
let buildersJsonPath: Path;
236+
let builderPaths: BuilderPaths;
202237

203238
// Read the `builders` entry of package.json.
204-
return this._host.read(normalize(pkgJsonPath)).pipe(
205-
concatMap(buffer =>
206-
of(parseJson(virtualFs.fileBufferToString(buffer), JsonParseMode.Loose))),
239+
return this._loadJsonFile(normalize(pkgJsonPath)).pipe(
207240
concatMap((pkgJson: JsonObject) => {
208241
const pkgJsonBuildersentry = pkgJson['builders'] as string;
209242
if (!pkgJsonBuildersentry) {
@@ -212,28 +245,40 @@ export class Architect {
212245

213246
buildersJsonPath = join(dirname(normalize(pkgJsonPath)), pkgJsonBuildersentry);
214247

215-
return this._host.read(buildersJsonPath);
248+
return this._loadJsonFile(buildersJsonPath);
216249
}),
217-
concatMap((buffer) => of(JSON.parse(virtualFs.fileBufferToString(buffer)))),
218250
// Validate builders json.
219-
concatMap((builderMap) =>
220-
this._validateAgainstSchema<BuilderMap>(builderMap, this._buildersSchema)),
221-
251+
concatMap((builderPathsMap) =>
252+
this._validateAgainstSchema<BuilderPathsMap>(builderPathsMap, this._buildersSchema)),
253+
concatMap((builderPathsMap) => {
254+
builderPaths = builderPathsMap.builders[builderName];
222255

223-
concatMap((builderMap) => {
224-
const builderDescription = builderMap.builders[builderName];
225-
226-
if (!builderDescription) {
256+
if (!builderPaths) {
227257
throw new BuilderCannotBeResolvedException(target.builder);
228258
}
229259

230-
// Resolve paths in the builder description.
260+
// Resolve paths in the builder paths.
231261
const builderJsonDir = dirname(buildersJsonPath);
232-
builderDescription.schema = join(builderJsonDir, builderDescription.schema);
233-
builderDescription.class = join(builderJsonDir, builderDescription.class);
262+
builderPaths.schema = join(builderJsonDir, builderPaths.schema);
263+
builderPaths.class = join(builderJsonDir, builderPaths.class);
264+
265+
// Save the builder paths so that we can lazily load the builder.
266+
this._builderPathsMap.set(target.builder, builderPaths);
234267

235-
// Validate options again builder schema.
236-
return of(builderDescription);
268+
// Load the schema.
269+
return this._loadJsonFile(builderPaths.schema);
270+
}),
271+
map(builderSchema => {
272+
const builderDescription = {
273+
name: target.builder,
274+
schema: builderSchema,
275+
description: builderPaths.description,
276+
};
277+
278+
// Save to cache before returning.
279+
this._builderDescriptionMap.set(builderDescription.name, builderDescription);
280+
281+
return builderDescription;
237282
}),
238283
).subscribe(obs);
239284
});
@@ -242,28 +287,44 @@ export class Architect {
242287
validateBuilderOptions<OptionsT>(
243288
target: Target<OptionsT>, builderDescription: BuilderDescription,
244289
): Observable<OptionsT> {
245-
return this._validateAgainstSchema<OptionsT>(target.options,
246-
normalize(builderDescription.schema));
290+
return this._validateAgainstSchema<OptionsT>(target.options, builderDescription.schema);
247291
}
248292

249293
getBuilder<OptionsT>(
250294
builderDescription: BuilderDescription, context: BuilderContext,
251295
): Builder<OptionsT> {
252-
// TODO: support more than the default export, maybe via builder#import-name.
253-
const builderModule = require(getSystemPath(builderDescription.class));
254-
const builderClass = builderModule['default'] as BuilderConstructor<OptionsT>;
296+
const name = builderDescription.name;
297+
let builderConstructor: BuilderConstructor<OptionsT>;
298+
299+
// Check cache for this builder.
300+
if (this._builderConstructorMap.has(name)) {
301+
builderConstructor = this._builderConstructorMap.get(name) as BuilderConstructor<OptionsT>;
302+
} else {
303+
if (!this._builderPathsMap.has(name)) {
304+
throw new BuilderNotFoundException(name);
305+
}
306+
307+
const builderPaths = this._builderPathsMap.get(name) as BuilderPaths;
255308

256-
return new builderClass(context);
309+
// TODO: support more than the default export, maybe via builder#import-name.
310+
const builderModule = require(getSystemPath(builderPaths.class));
311+
builderConstructor = builderModule['default'] as BuilderConstructor<OptionsT>;
312+
313+
// Save builder to cache before returning.
314+
this._builderConstructorMap.set(builderDescription.name, builderConstructor);
315+
}
316+
317+
const builder = new builderConstructor(context);
318+
319+
return builder;
257320
}
258321

259322
// Warning: this method changes contentJson in place.
260323
// TODO: add transforms to resolve paths.
261-
private _validateAgainstSchema<T = {}>(contentJson: {}, schemaPath: Path): Observable<T> {
324+
private _validateAgainstSchema<T = {}>(contentJson: {}, schemaJson: JsonObject): Observable<T> {
262325
const registry = new schema.CoreSchemaRegistry();
263326

264-
return this._host.read(schemaPath).pipe(
265-
concatMap((buffer) => of(JSON.parse(virtualFs.fileBufferToString(buffer)))),
266-
concatMap((schemaContent) => registry.compile(schemaContent)),
327+
return registry.compile(schemaJson).pipe(
267328
concatMap(validator => validator(contentJson)),
268329
concatMap(validatorResult => {
269330
if (validatorResult.success) {
@@ -274,4 +335,11 @@ export class Architect {
274335
}),
275336
);
276337
}
338+
339+
private _loadJsonFile(path: Path): Observable<JsonObject> {
340+
return this._host.read(normalize(path)).pipe(
341+
map(buffer => virtualFs.fileBufferToString(buffer)),
342+
map(str => parseJson(str, JsonParseMode.Loose) as {} as JsonObject),
343+
);
344+
}
277345
}

packages/angular_devkit/architect/src/builder.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import { Path, logging, virtualFs } from '@angular-devkit/core';
9+
import { JsonObject, Path, logging, virtualFs } from '@angular-devkit/core';
1010
import { Observable } from 'rxjs/Observable';
1111
import { Architect, Target } from './architect';
1212

@@ -20,7 +20,6 @@ export interface BuilderContext {
2020
// TODO: use Build Event Protocol
2121
// https://docs.bazel.build/versions/master/build-event-protocol.html
2222
// https://github.com/googleapis/googleapis/tree/master/google/devtools/build/v1
23-
// http://dcode.io/protobuf.js/Message.html#toJSON
2423
export interface BuildEvent {
2524
success: boolean;
2625
}
@@ -29,16 +28,22 @@ export interface Builder<OptionsT> {
2928
run(_target: Target<Partial<OptionsT>>): Observable<BuildEvent>;
3029
}
3130

32-
export interface BuilderMap {
33-
builders: { [k: string]: BuilderDescription };
31+
export interface BuilderPathsMap {
32+
builders: { [k: string]: BuilderPaths };
3433
}
3534

36-
export interface BuilderDescription {
35+
export interface BuilderPaths {
3736
class: Path;
3837
schema: Path;
3938
description: string;
4039
}
4140

41+
export interface BuilderDescription {
42+
name: string;
43+
schema: JsonObject;
44+
description: string;
45+
}
46+
4247
export interface BuilderConstructor<OptionsT> {
4348
new(context: BuilderContext): Builder<OptionsT>;
4449
}

0 commit comments

Comments
 (0)