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

Commit 68daef3

Browse files
committed
feat(@angular-devkit/schematics): support collection extension
1 parent 3c26a73 commit 68daef3

File tree

16 files changed

+343
-16
lines changed

16 files changed

+343
-16
lines changed

packages/angular_devkit/schematics/collection-schema.json

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@
44
"title": "Collection Schema for validating a 'collection.json'.",
55
"type": "object",
66
"properties": {
7+
"extends": {
8+
"oneOf": [
9+
{
10+
"type": "string",
11+
"minLength": 1
12+
},
13+
{
14+
"type": "array",
15+
"items": {
16+
"type": "string",
17+
"minLength": 1
18+
},
19+
"minItems": 1
20+
}
21+
]
22+
},
723
"schematics": {
824
"type": "object",
925
"description": "A map of schematic names to schematic details",

packages/angular_devkit/schematics/src/engine/collection.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { Collection, CollectionDescription, Schematic } from './interface';
1212
export class CollectionImpl<CollectionT extends object, SchematicT extends object>
1313
implements Collection<CollectionT, SchematicT> {
1414
constructor(private _description: CollectionDescription<CollectionT>,
15-
private _engine: SchematicEngine<CollectionT, SchematicT>) {
15+
private _engine: SchematicEngine<CollectionT, SchematicT>,
16+
public readonly baseDescriptions?: Array<CollectionDescription<CollectionT>>) {
1617
}
1718

1819
get description() { return this._description; }

packages/angular_devkit/schematics/src/engine/engine.ts

+60-9
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ export class UnknownUrlSourceProtocol extends BaseException {
4040
export class UnknownCollectionException extends BaseException {
4141
constructor(name: string) { super(`Unknown collection "${name}".`); }
4242
}
43+
44+
export class CircularCollectionException extends BaseException {
45+
constructor(name: string) {
46+
super(`Circular collection reference "${name}".`);
47+
}
48+
}
49+
4350
export class UnknownSchematicException extends BaseException {
4451
constructor(name: string, collection: CollectionDescription<{}>) {
4552
super(`Schematic "${name}" not found in collection "${collection.name}".`);
@@ -76,16 +83,38 @@ export class SchematicEngine<CollectionT extends object, SchematicT extends obje
7683
return collection;
7784
}
7885

86+
const [description, bases] = this._createCollectionDescription(name);
87+
88+
collection = new CollectionImpl<CollectionT, SchematicT>(description, this, bases);
89+
this._collectionCache.set(name, collection);
90+
this._schematicCache.set(name, new Map());
91+
92+
return collection;
93+
}
94+
95+
private _createCollectionDescription(
96+
name: string,
97+
parentNames?: Set<string>,
98+
): [CollectionDescription<CollectionT>, Array<CollectionDescription<CollectionT>>] {
7999
const description = this._host.createCollectionDescription(name);
80100
if (!description) {
81101
throw new UnknownCollectionException(name);
82102
}
103+
if (parentNames && parentNames.has(description.name)) {
104+
throw new CircularCollectionException(name);
105+
}
83106

84-
collection = new CollectionImpl<CollectionT, SchematicT>(description, this);
85-
this._collectionCache.set(name, collection);
86-
this._schematicCache.set(name, new Map());
107+
const bases = new Array<CollectionDescription<CollectionT>>();
108+
if (description.extends) {
109+
parentNames = (parentNames || new Set<string>()).add(description.name);
110+
for (const baseName of description.extends) {
111+
const [base, baseBases] = this._createCollectionDescription(baseName, new Set(parentNames));
87112

88-
return collection;
113+
bases.unshift(base, ...baseBases);
114+
}
115+
}
116+
117+
return [description, bases];
89118
}
90119

91120
createContext(
@@ -148,21 +177,43 @@ export class SchematicEngine<CollectionT extends object, SchematicT extends obje
148177
return schematic;
149178
}
150179

151-
const description = this._host.createSchematicDescription(name, collection.description);
180+
let collectionDescription = collection.description;
181+
let description = this._host.createSchematicDescription(name, collection.description);
152182
if (!description) {
153-
throw new UnknownSchematicException(name, collection.description);
183+
if (collection.baseDescriptions) {
184+
for (const base of collection.baseDescriptions) {
185+
description = this._host.createSchematicDescription(name, base);
186+
if (description) {
187+
collectionDescription = base;
188+
break;
189+
}
190+
}
191+
}
192+
if (!description) {
193+
// Report the error for the top level schematic collection
194+
throw new UnknownSchematicException(name, collection.description);
195+
}
154196
}
155197

156-
const factory = this._host.getSchematicRuleFactory(description, collection.description);
198+
const factory = this._host.getSchematicRuleFactory(description, collectionDescription);
157199
schematic = new SchematicImpl<CollectionT, SchematicT>(description, factory, collection, this);
158200

159201
schematicMap.set(name, schematic);
160202

161203
return schematic;
162204
}
163205

164-
listSchematicNames(collection: Collection<CollectionT, SchematicT>) {
165-
return this._host.listSchematicNames(collection.description);
206+
listSchematicNames(collection: Collection<CollectionT, SchematicT>): string[] {
207+
const names = this._host.listSchematicNames(collection.description);
208+
209+
if (collection.baseDescriptions) {
210+
for (const base of collection.baseDescriptions) {
211+
names.push(...this._host.listSchematicNames(base));
212+
}
213+
}
214+
215+
// remove duplicates
216+
return [...new Set(names)];
166217
}
167218

168219
transformOptions<OptionT extends object, ResultT extends object>(

packages/angular_devkit/schematics/src/engine/interface.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { TaskConfigurationGenerator, TaskExecutor, TaskId } from './task';
1919
*/
2020
export type CollectionDescription<CollectionMetadataT extends object> = CollectionMetadataT & {
2121
readonly name: string;
22+
readonly extends?: string[];
2223
};
2324

2425
/**
@@ -49,7 +50,7 @@ export interface EngineHost<CollectionMetadataT extends object, SchematicMetadat
4950
createSchematicDescription(
5051
name: string,
5152
collection: CollectionDescription<CollectionMetadataT>):
52-
SchematicDescription<CollectionMetadataT, SchematicMetadataT>;
53+
SchematicDescription<CollectionMetadataT, SchematicMetadataT> | null;
5354
getSchematicRuleFactory<OptionT extends object>(
5455
schematic: SchematicDescription<CollectionMetadataT, SchematicMetadataT>,
5556
collection: CollectionDescription<CollectionMetadataT>): RuleFactory<OptionT>;
@@ -108,6 +109,7 @@ export interface Engine<CollectionMetadataT extends object, SchematicMetadataT e
108109
*/
109110
export interface Collection<CollectionMetadataT extends object, SchematicMetadataT extends object> {
110111
readonly description: CollectionDescription<CollectionMetadataT>;
112+
readonly baseDescriptions?: Array<CollectionDescription<CollectionMetadataT>>;
111113

112114
createSchematic(name: string): Schematic<CollectionMetadataT, SchematicMetadataT>;
113115
listSchematicNames(): string[];

packages/angular_devkit/schematics/tools/fallback-engine-host.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@ export class FallbackEngineHost implements EngineHost<{}, {}> {
6868
createSchematicDescription(
6969
name: string,
7070
collection: CollectionDescription<FallbackCollectionDescription>,
71-
): SchematicDescription<FallbackCollectionDescription, FallbackSchematicDescription> {
71+
): SchematicDescription<FallbackCollectionDescription, FallbackSchematicDescription> | null {
7272
const description = collection.host.createSchematicDescription(name, collection.description);
73+
if (!description) {
74+
return null;
75+
}
7376

7477
return { name, collection, description };
7578
}

packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
Source,
2121
TaskExecutor,
2222
TaskExecutorFactory,
23-
UnknownSchematicException,
2423
UnregisteredTaskException,
2524
} from '../src';
2625
import {
@@ -142,6 +141,11 @@ export abstract class FileSystemEngineHostBase implements
142141
throw new InvalidCollectionJsonException(name, path);
143142
}
144143

144+
// normalize extends property to an array
145+
if (typeof jsonValue['extends'] === 'string') {
146+
jsonValue['extends'] = [jsonValue['extends']];
147+
}
148+
145149
const description = this._transformCollectionDescription(name, {
146150
...jsonValue,
147151
path,
@@ -169,7 +173,7 @@ export abstract class FileSystemEngineHostBase implements
169173
createSchematicDescription(
170174
name: string,
171175
collection: FileSystemCollectionDesc,
172-
): FileSystemSchematicDesc {
176+
): FileSystemSchematicDesc | null {
173177
// Resolve aliases first.
174178
for (const schematicName of Object.keys(collection.schematics)) {
175179
const schematicDescription = collection.schematics[schematicName];
@@ -180,13 +184,13 @@ export abstract class FileSystemEngineHostBase implements
180184
}
181185

182186
if (!(name in collection.schematics)) {
183-
throw new UnknownSchematicException(name, collection);
187+
return null;
184188
}
185189

186190
const collectionPath = dirname(collection.path);
187191
const partialDesc: Partial<FileSystemSchematicDesc> | null = collection.schematics[name];
188192
if (!partialDesc) {
189-
throw new UnknownSchematicException(name, collection);
193+
return null;
190194
}
191195

192196
if (partialDesc.extends) {

packages/angular_devkit/schematics/tools/file-system-engine-host_spec.ts

+157
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,163 @@ describe('FileSystemEngineHost', () => {
3939
expect(schematic1.description.name).toBe('schematic1');
4040
});
4141

42+
it('lists schematics but not aliases', () => {
43+
const engineHost = new FileSystemEngineHost(root);
44+
const engine = new SchematicEngine(engineHost);
45+
46+
const testCollection = engine.createCollection('aliases');
47+
const names = testCollection.listSchematicNames();
48+
49+
expect(names).not.toBeNull();
50+
expect(names[0]).toBe('schematic1');
51+
expect(names[1]).toBe('schematic2');
52+
});
53+
54+
it('extends a collection with string', () => {
55+
const engineHost = new FileSystemEngineHost(root);
56+
const engine = new SchematicEngine(engineHost);
57+
58+
const testCollection = engine.createCollection('extends-basic-string');
59+
60+
expect(testCollection.baseDescriptions).not.toBeUndefined();
61+
expect(testCollection.baseDescriptions
62+
&& testCollection.baseDescriptions.length).toBe(1);
63+
64+
const schematic1 = engine.createSchematic('schematic1', testCollection);
65+
66+
expect(schematic1).not.toBeNull();
67+
expect(schematic1.description.name).toBe('schematic1');
68+
69+
const schematic2 = engine.createSchematic('schematic2', testCollection);
70+
71+
expect(schematic2).not.toBeNull();
72+
expect(schematic2.description.name).toBe('schematic2');
73+
74+
const names = testCollection.listSchematicNames();
75+
76+
expect(names.length).toBe(2);
77+
});
78+
79+
it('extends a collection with array', () => {
80+
const engineHost = new FileSystemEngineHost(root);
81+
const engine = new SchematicEngine(engineHost);
82+
83+
const testCollection = engine.createCollection('extends-basic');
84+
85+
expect(testCollection.baseDescriptions).not.toBeUndefined();
86+
expect(testCollection.baseDescriptions
87+
&& testCollection.baseDescriptions.length).toBe(1);
88+
89+
const schematic1 = engine.createSchematic('schematic1', testCollection);
90+
91+
expect(schematic1).not.toBeNull();
92+
expect(schematic1.description.name).toBe('schematic1');
93+
94+
const schematic2 = engine.createSchematic('schematic2', testCollection);
95+
96+
expect(schematic2).not.toBeNull();
97+
expect(schematic2.description.name).toBe('schematic2');
98+
99+
const names = testCollection.listSchematicNames();
100+
101+
expect(names.length).toBe(2);
102+
});
103+
104+
it('extends a collection with full depth', () => {
105+
const engineHost = new FileSystemEngineHost(root);
106+
const engine = new SchematicEngine(engineHost);
107+
108+
const testCollection = engine.createCollection('extends-deep');
109+
110+
expect(testCollection.baseDescriptions).not.toBeUndefined();
111+
expect(testCollection.baseDescriptions
112+
&& testCollection.baseDescriptions.length).toBe(2);
113+
114+
const schematic1 = engine.createSchematic('schematic1', testCollection);
115+
116+
expect(schematic1).not.toBeNull();
117+
expect(schematic1.description.name).toBe('schematic1');
118+
119+
const schematic2 = engine.createSchematic('schematic2', testCollection);
120+
121+
expect(schematic2).not.toBeNull();
122+
expect(schematic2.description.name).toBe('schematic2');
123+
124+
const names = testCollection.listSchematicNames();
125+
126+
expect(names.length).toBe(2);
127+
});
128+
129+
it('replaces base schematics when extending', () => {
130+
const engineHost = new FileSystemEngineHost(root);
131+
const engine = new SchematicEngine(engineHost);
132+
133+
const testCollection = engine.createCollection('extends-replace');
134+
135+
expect(testCollection.baseDescriptions).not.toBeUndefined();
136+
expect(testCollection.baseDescriptions
137+
&& testCollection.baseDescriptions.length).toBe(1);
138+
139+
const schematic1 = engine.createSchematic('schematic1', testCollection);
140+
141+
expect(schematic1).not.toBeNull();
142+
expect(schematic1.description.name).toBe('schematic1');
143+
expect(schematic1.description.description).toBe('replaced');
144+
145+
const names = testCollection.listSchematicNames();
146+
147+
expect(names).not.toBeNull();
148+
expect(names.length).toBe(1);
149+
});
150+
151+
it('extends multiple collections', () => {
152+
const engineHost = new FileSystemEngineHost(root);
153+
const engine = new SchematicEngine(engineHost);
154+
155+
const testCollection = engine.createCollection('extends-multiple');
156+
157+
expect(testCollection.baseDescriptions).not.toBeUndefined();
158+
expect(testCollection.baseDescriptions
159+
&& testCollection.baseDescriptions.length).toBe(4);
160+
161+
const schematic1 = engine.createSchematic('schematic1', testCollection);
162+
163+
expect(schematic1).not.toBeNull();
164+
expect(schematic1.description.name).toBe('schematic1');
165+
expect(schematic1.description.description).toBe('replaced');
166+
167+
const schematic2 = engine.createSchematic('schematic2', testCollection);
168+
169+
expect(schematic2).not.toBeNull();
170+
expect(schematic2.description.name).toBe('schematic2');
171+
172+
const names = testCollection.listSchematicNames();
173+
174+
expect(names).not.toBeNull();
175+
expect(names.length).toBe(2);
176+
});
177+
178+
it('errors on simple circular collections', () => {
179+
const engineHost = new FileSystemEngineHost(root);
180+
const engine = new SchematicEngine(engineHost);
181+
182+
expect(() => engine.createCollection('extends-circular')).toThrow();
183+
});
184+
185+
it('errors on complex circular collections', () => {
186+
const engineHost = new FileSystemEngineHost(root);
187+
const engine = new SchematicEngine(engineHost);
188+
189+
expect(() => engine.createCollection('extends-circular-multiple')).toThrow();
190+
});
191+
192+
it('errors on deep circular collections', () => {
193+
const engineHost = new FileSystemEngineHost(root);
194+
const engine = new SchematicEngine(engineHost);
195+
196+
expect(() => engine.createCollection('extends-circular-deep')).toThrow();
197+
});
198+
42199
it('errors on invalid aliases', () => {
43200
const engineHost = new FileSystemEngineHost(root);
44201
const engine = new SchematicEngine(engineHost);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "extends-basic-string",
3+
"extends": "works",
4+
"schematics": {
5+
"schematic2": {
6+
"description": "2",
7+
"factory": "../null-factory"
8+
}
9+
}
10+
}

0 commit comments

Comments
 (0)