Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

feat(@angular-devkit/schematics): support collection extension #398

Merged
merged 1 commit into from
Feb 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/angular_devkit/schematics/collection-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
"title": "Collection Schema for validating a 'collection.json'.",
"type": "object",
"properties": {
"extends": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How hard to add type: [ "array", "string" ] ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added support for both.

"oneOf": [
{
"type": "string",
"minLength": 1
},
{
"type": "array",
"items": {
"type": "string",
"minLength": 1
},
"minItems": 1
}
]
},
"schematics": {
"type": "object",
"description": "A map of schematic names to schematic details",
Expand Down
3 changes: 2 additions & 1 deletion packages/angular_devkit/schematics/src/engine/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { Collection, CollectionDescription, Schematic } from './interface';
export class CollectionImpl<CollectionT extends object, SchematicT extends object>
implements Collection<CollectionT, SchematicT> {
constructor(private _description: CollectionDescription<CollectionT>,
private _engine: SchematicEngine<CollectionT, SchematicT>) {
private _engine: SchematicEngine<CollectionT, SchematicT>,
public readonly baseDescriptions?: Array<CollectionDescription<CollectionT>>) {
}

get description() { return this._description; }
Expand Down
69 changes: 60 additions & 9 deletions packages/angular_devkit/schematics/src/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export class UnknownUrlSourceProtocol extends BaseException {
export class UnknownCollectionException extends BaseException {
constructor(name: string) { super(`Unknown collection "${name}".`); }
}

export class CircularCollectionException extends BaseException {
constructor(name: string) {
super(`Circular collection reference "${name}".`);
}
}

export class UnknownSchematicException extends BaseException {
constructor(name: string, collection: CollectionDescription<{}>) {
super(`Schematic "${name}" not found in collection "${collection.name}".`);
Expand Down Expand Up @@ -76,16 +83,38 @@ export class SchematicEngine<CollectionT extends object, SchematicT extends obje
return collection;
}

const [description, bases] = this._createCollectionDescription(name);

collection = new CollectionImpl<CollectionT, SchematicT>(description, this, bases);
this._collectionCache.set(name, collection);
this._schematicCache.set(name, new Map());

return collection;
}

private _createCollectionDescription(
name: string,
parentNames?: Set<string>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one nit but not stopping the approval; parentNames = new Set<string>() then lower just add regardless of whether it's defined or not.

): [CollectionDescription<CollectionT>, Array<CollectionDescription<CollectionT>>] {
const description = this._host.createCollectionDescription(name);
if (!description) {
throw new UnknownCollectionException(name);
}
if (parentNames && parentNames.has(description.name)) {
throw new CircularCollectionException(name);
}

collection = new CollectionImpl<CollectionT, SchematicT>(description, this);
this._collectionCache.set(name, collection);
this._schematicCache.set(name, new Map());
const bases = new Array<CollectionDescription<CollectionT>>();
if (description.extends) {
parentNames = (parentNames || new Set<string>()).add(description.name);
for (const baseName of description.extends) {
const [base, baseBases] = this._createCollectionDescription(baseName, new Set(parentNames));

return collection;
bases.unshift(base, ...baseBases);
}
}

return [description, bases];
}

createContext(
Expand Down Expand Up @@ -148,21 +177,43 @@ export class SchematicEngine<CollectionT extends object, SchematicT extends obje
return schematic;
}

const description = this._host.createSchematicDescription(name, collection.description);
let collectionDescription = collection.description;
let description = this._host.createSchematicDescription(name, collection.description);
if (!description) {
throw new UnknownSchematicException(name, collection.description);
if (collection.baseDescriptions) {
for (const base of collection.baseDescriptions) {
description = this._host.createSchematicDescription(name, base);
if (description) {
collectionDescription = base;
break;
}
}
}
if (!description) {
// Report the error for the top level schematic collection
throw new UnknownSchematicException(name, collection.description);
}
}

const factory = this._host.getSchematicRuleFactory(description, collection.description);
const factory = this._host.getSchematicRuleFactory(description, collectionDescription);
schematic = new SchematicImpl<CollectionT, SchematicT>(description, factory, collection, this);

schematicMap.set(name, schematic);

return schematic;
}

listSchematicNames(collection: Collection<CollectionT, SchematicT>) {
return this._host.listSchematicNames(collection.description);
listSchematicNames(collection: Collection<CollectionT, SchematicT>): string[] {
const names = this._host.listSchematicNames(collection.description);

if (collection.baseDescriptions) {
for (const base of collection.baseDescriptions) {
names.push(...this._host.listSchematicNames(base));
}
}

// remove duplicates
return [...new Set(names)];
}

transformOptions<OptionT extends object, ResultT extends object>(
Expand Down
4 changes: 3 additions & 1 deletion packages/angular_devkit/schematics/src/engine/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { TaskConfigurationGenerator, TaskExecutor, TaskId } from './task';
*/
export type CollectionDescription<CollectionMetadataT extends object> = CollectionMetadataT & {
readonly name: string;
readonly extends?: string[];
};

/**
Expand Down Expand Up @@ -49,7 +50,7 @@ export interface EngineHost<CollectionMetadataT extends object, SchematicMetadat
createSchematicDescription(
name: string,
collection: CollectionDescription<CollectionMetadataT>):
SchematicDescription<CollectionMetadataT, SchematicMetadataT>;
SchematicDescription<CollectionMetadataT, SchematicMetadataT> | null;
getSchematicRuleFactory<OptionT extends object>(
schematic: SchematicDescription<CollectionMetadataT, SchematicMetadataT>,
collection: CollectionDescription<CollectionMetadataT>): RuleFactory<OptionT>;
Expand Down Expand Up @@ -108,6 +109,7 @@ export interface Engine<CollectionMetadataT extends object, SchematicMetadataT e
*/
export interface Collection<CollectionMetadataT extends object, SchematicMetadataT extends object> {
readonly description: CollectionDescription<CollectionMetadataT>;
readonly baseDescriptions?: Array<CollectionDescription<CollectionMetadataT>>;

createSchematic(name: string): Schematic<CollectionMetadataT, SchematicMetadataT>;
listSchematicNames(): string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ export class FallbackEngineHost implements EngineHost<{}, {}> {
createSchematicDescription(
name: string,
collection: CollectionDescription<FallbackCollectionDescription>,
): SchematicDescription<FallbackCollectionDescription, FallbackSchematicDescription> {
): SchematicDescription<FallbackCollectionDescription, FallbackSchematicDescription> | null {
const description = collection.host.createSchematicDescription(name, collection.description);
if (!description) {
return null;
}

return { name, collection, description };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
Source,
TaskExecutor,
TaskExecutorFactory,
UnknownSchematicException,
UnregisteredTaskException,
} from '../src';
import {
Expand Down Expand Up @@ -142,6 +141,11 @@ export abstract class FileSystemEngineHostBase implements
throw new InvalidCollectionJsonException(name, path);
}

// normalize extends property to an array
if (typeof jsonValue['extends'] === 'string') {
jsonValue['extends'] = [jsonValue['extends']];
}

const description = this._transformCollectionDescription(name, {
...jsonValue,
path,
Expand Down Expand Up @@ -169,7 +173,7 @@ export abstract class FileSystemEngineHostBase implements
createSchematicDescription(
name: string,
collection: FileSystemCollectionDesc,
): FileSystemSchematicDesc {
): FileSystemSchematicDesc | null {
// Resolve aliases first.
for (const schematicName of Object.keys(collection.schematics)) {
const schematicDescription = collection.schematics[schematicName];
Expand All @@ -180,13 +184,13 @@ export abstract class FileSystemEngineHostBase implements
}

if (!(name in collection.schematics)) {
throw new UnknownSchematicException(name, collection);
return null;
}

const collectionPath = dirname(collection.path);
const partialDesc: Partial<FileSystemSchematicDesc> | null = collection.schematics[name];
if (!partialDesc) {
throw new UnknownSchematicException(name, collection);
return null;
}

if (partialDesc.extends) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,163 @@ describe('FileSystemEngineHost', () => {
expect(schematic1.description.name).toBe('schematic1');
});

it('lists schematics but not aliases', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

const testCollection = engine.createCollection('aliases');
const names = testCollection.listSchematicNames();

expect(names).not.toBeNull();
expect(names[0]).toBe('schematic1');
expect(names[1]).toBe('schematic2');
});

it('extends a collection with string', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

const testCollection = engine.createCollection('extends-basic-string');

expect(testCollection.baseDescriptions).not.toBeUndefined();
expect(testCollection.baseDescriptions
&& testCollection.baseDescriptions.length).toBe(1);

const schematic1 = engine.createSchematic('schematic1', testCollection);

expect(schematic1).not.toBeNull();
expect(schematic1.description.name).toBe('schematic1');

const schematic2 = engine.createSchematic('schematic2', testCollection);

expect(schematic2).not.toBeNull();
expect(schematic2.description.name).toBe('schematic2');

const names = testCollection.listSchematicNames();

expect(names.length).toBe(2);
});

it('extends a collection with array', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

const testCollection = engine.createCollection('extends-basic');

expect(testCollection.baseDescriptions).not.toBeUndefined();
expect(testCollection.baseDescriptions
&& testCollection.baseDescriptions.length).toBe(1);

const schematic1 = engine.createSchematic('schematic1', testCollection);

expect(schematic1).not.toBeNull();
expect(schematic1.description.name).toBe('schematic1');

const schematic2 = engine.createSchematic('schematic2', testCollection);

expect(schematic2).not.toBeNull();
expect(schematic2.description.name).toBe('schematic2');

const names = testCollection.listSchematicNames();

expect(names.length).toBe(2);
});

it('extends a collection with full depth', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

const testCollection = engine.createCollection('extends-deep');

expect(testCollection.baseDescriptions).not.toBeUndefined();
expect(testCollection.baseDescriptions
&& testCollection.baseDescriptions.length).toBe(2);

const schematic1 = engine.createSchematic('schematic1', testCollection);

expect(schematic1).not.toBeNull();
expect(schematic1.description.name).toBe('schematic1');

const schematic2 = engine.createSchematic('schematic2', testCollection);

expect(schematic2).not.toBeNull();
expect(schematic2.description.name).toBe('schematic2');

const names = testCollection.listSchematicNames();

expect(names.length).toBe(2);
});

it('replaces base schematics when extending', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

const testCollection = engine.createCollection('extends-replace');

expect(testCollection.baseDescriptions).not.toBeUndefined();
expect(testCollection.baseDescriptions
&& testCollection.baseDescriptions.length).toBe(1);

const schematic1 = engine.createSchematic('schematic1', testCollection);

expect(schematic1).not.toBeNull();
expect(schematic1.description.name).toBe('schematic1');
expect(schematic1.description.description).toBe('replaced');

const names = testCollection.listSchematicNames();

expect(names).not.toBeNull();
expect(names.length).toBe(1);
});

it('extends multiple collections', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

const testCollection = engine.createCollection('extends-multiple');

expect(testCollection.baseDescriptions).not.toBeUndefined();
expect(testCollection.baseDescriptions
&& testCollection.baseDescriptions.length).toBe(4);

const schematic1 = engine.createSchematic('schematic1', testCollection);

expect(schematic1).not.toBeNull();
expect(schematic1.description.name).toBe('schematic1');
expect(schematic1.description.description).toBe('replaced');

const schematic2 = engine.createSchematic('schematic2', testCollection);

expect(schematic2).not.toBeNull();
expect(schematic2.description.name).toBe('schematic2');

const names = testCollection.listSchematicNames();

expect(names).not.toBeNull();
expect(names.length).toBe(2);
});

it('errors on simple circular collections', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

expect(() => engine.createCollection('extends-circular')).toThrow();
});

it('errors on complex circular collections', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

expect(() => engine.createCollection('extends-circular-multiple')).toThrow();
});

it('errors on deep circular collections', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);

expect(() => engine.createCollection('extends-circular-deep')).toThrow();
});

it('errors on invalid aliases', () => {
const engineHost = new FileSystemEngineHost(root);
const engine = new SchematicEngine(engineHost);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "extends-basic-string",
"extends": "works",
"schematics": {
"schematic2": {
"description": "2",
"factory": "../null-factory"
}
}
}
Loading