mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-22 15:02:11 +08:00
feat(@angular-devkit/schematics): support executing a schematic rule on a subtree
This commit is contained in:
parent
5c73ee3537
commit
a0ac4b0e3d
@ -218,6 +218,7 @@ export interface EngineHost<CollectionMetadataT extends object, SchematicMetadat
|
|||||||
|
|
||||||
export interface ExecutionOptions {
|
export interface ExecutionOptions {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
|
scope: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function externalSchematic<OptionT extends object>(collectionName: string, schematicName: string, options: OptionT, executionOptions?: Partial<ExecutionOptions>): Rule;
|
export declare function externalSchematic<OptionT extends object>(collectionName: string, schematicName: string, options: OptionT, executionOptions?: Partial<ExecutionOptions>): Rule;
|
||||||
|
@ -42,6 +42,7 @@ export interface TaskInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionOptions {
|
export interface ExecutionOptions {
|
||||||
|
scope: string;
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import { Observable, of as observableOf } from 'rxjs';
|
|||||||
import { concatMap, first, map } from 'rxjs/operators';
|
import { concatMap, first, map } from 'rxjs/operators';
|
||||||
import { callRule } from '../rules/call';
|
import { callRule } from '../rules/call';
|
||||||
import { Tree } from '../tree/interface';
|
import { Tree } from '../tree/interface';
|
||||||
|
import { ScopedTree } from '../tree/scoped';
|
||||||
import {
|
import {
|
||||||
Collection,
|
Collection,
|
||||||
Engine,
|
Engine,
|
||||||
@ -58,7 +59,28 @@ export class SchematicImpl<CollectionT extends object, SchematicT extends object
|
|||||||
map(o => [tree, o]),
|
map(o => [tree, o]),
|
||||||
)),
|
)),
|
||||||
concatMap(([tree, transformedOptions]: [Tree, OptionT]) => {
|
concatMap(([tree, transformedOptions]: [Tree, OptionT]) => {
|
||||||
return callRule(this._factory(transformedOptions), observableOf(tree), context);
|
let input: Tree;
|
||||||
|
let scoped = false;
|
||||||
|
if (executionOptions && executionOptions.scope) {
|
||||||
|
scoped = true;
|
||||||
|
input = new ScopedTree(tree, executionOptions.scope);
|
||||||
|
} else {
|
||||||
|
input = tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
return callRule(this._factory(transformedOptions), observableOf(input), context).pipe(
|
||||||
|
map(output => {
|
||||||
|
if (output === input) {
|
||||||
|
return tree;
|
||||||
|
} else if (scoped) {
|
||||||
|
tree.merge(output);
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
} else {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -147,4 +147,24 @@ describe('Schematic', () => {
|
|||||||
.then(done, done.fail);
|
.then(done, done.fail);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can be called with a scope', done => {
|
||||||
|
const desc: SchematicDescription<CollectionT, SchematicT> = {
|
||||||
|
collection,
|
||||||
|
name: 'test',
|
||||||
|
description: '',
|
||||||
|
path: '/a/b/c',
|
||||||
|
factory: () => (tree: Tree) => {
|
||||||
|
tree.create('a/b/c', 'some content');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const schematic = new SchematicImpl(desc, desc.factory, null !, engine);
|
||||||
|
schematic.call({}, observableOf(empty()), {}, { scope: 'base' })
|
||||||
|
.toPromise()
|
||||||
|
.then(x => {
|
||||||
|
expect(files(x)).toEqual(['/base/a/b/c']);
|
||||||
|
})
|
||||||
|
.then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
139
packages/angular_devkit/schematics/src/tree/scoped.ts
Normal file
139
packages/angular_devkit/schematics/src/tree/scoped.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* @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 {
|
||||||
|
NormalizedRoot,
|
||||||
|
Path,
|
||||||
|
PathFragment,
|
||||||
|
join,
|
||||||
|
normalize,
|
||||||
|
relative,
|
||||||
|
} from '@angular-devkit/core';
|
||||||
|
import { Action } from './action';
|
||||||
|
import {
|
||||||
|
DirEntry,
|
||||||
|
FileEntry,
|
||||||
|
FileVisitor,
|
||||||
|
MergeStrategy,
|
||||||
|
Tree,
|
||||||
|
TreeSymbol,
|
||||||
|
UpdateRecorder,
|
||||||
|
} from './interface';
|
||||||
|
|
||||||
|
class ScopedFileEntry implements FileEntry {
|
||||||
|
constructor(private _base: FileEntry, private scope: Path) {}
|
||||||
|
|
||||||
|
get path(): Path {
|
||||||
|
return join(NormalizedRoot, relative(this.scope, this._base.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
get content(): Buffer { return this._base.content; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedDirEntry implements DirEntry {
|
||||||
|
constructor(private _base: DirEntry, readonly scope: Path) {}
|
||||||
|
|
||||||
|
get parent(): DirEntry | null {
|
||||||
|
if (!this._base.parent || this._base.path == this.scope) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ScopedDirEntry(this._base.parent, this.scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
get path(): Path {
|
||||||
|
return join(NormalizedRoot, relative(this.scope, this._base.path));
|
||||||
|
}
|
||||||
|
|
||||||
|
get subdirs(): PathFragment[] {
|
||||||
|
return this._base.subdirs;
|
||||||
|
}
|
||||||
|
get subfiles(): PathFragment[] {
|
||||||
|
return this._base.subfiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
dir(name: PathFragment): DirEntry {
|
||||||
|
const entry = this._base.dir(name);
|
||||||
|
|
||||||
|
return entry && new ScopedDirEntry(entry, this.scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
file(name: PathFragment): FileEntry | null {
|
||||||
|
const entry = this._base.file(name);
|
||||||
|
|
||||||
|
return entry && new ScopedFileEntry(entry, this.scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(visitor: FileVisitor): void {
|
||||||
|
return this._base.visit((path, entry) => {
|
||||||
|
visitor(
|
||||||
|
join(NormalizedRoot, relative(this.scope, path)),
|
||||||
|
entry && new ScopedFileEntry(entry, this.scope),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScopedTree implements Tree {
|
||||||
|
readonly _root: ScopedDirEntry;
|
||||||
|
|
||||||
|
constructor(private _base: Tree, scope: string) {
|
||||||
|
const normalizedScope = normalize('/' + scope);
|
||||||
|
this._root = new ScopedDirEntry(this._base.getDir(normalizedScope), normalizedScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
get root(): DirEntry { return this._root; }
|
||||||
|
|
||||||
|
branch(): Tree { return new ScopedTree(this._base.branch(), this._root.scope); }
|
||||||
|
merge(other: Tree, strategy?: MergeStrategy): void { this._base.merge(other, strategy); }
|
||||||
|
|
||||||
|
// Readonly.
|
||||||
|
read(path: string): Buffer | null { return this._base.read(this._fullPath(path)); }
|
||||||
|
exists(path: string): boolean { return this._base.exists(this._fullPath(path)); }
|
||||||
|
get(path: string): FileEntry | null {
|
||||||
|
const entry = this._base.get(this._fullPath(path));
|
||||||
|
|
||||||
|
return entry && new ScopedFileEntry(entry, this._root.scope);
|
||||||
|
}
|
||||||
|
getDir(path: string): DirEntry {
|
||||||
|
const entry = this._base.getDir(this._fullPath(path));
|
||||||
|
|
||||||
|
return entry && new ScopedDirEntry(entry, this._root.scope);
|
||||||
|
}
|
||||||
|
visit(visitor: FileVisitor): void { return this._root.visit(visitor); }
|
||||||
|
|
||||||
|
// Change content of host files.
|
||||||
|
overwrite(path: string, content: Buffer | string): void {
|
||||||
|
return this._base.overwrite(this._fullPath(path), content);
|
||||||
|
}
|
||||||
|
beginUpdate(path: string): UpdateRecorder {
|
||||||
|
return this._base.beginUpdate(this._fullPath(path));
|
||||||
|
}
|
||||||
|
commitUpdate(record: UpdateRecorder): void { return this._base.commitUpdate(record); }
|
||||||
|
|
||||||
|
// Structural methods.
|
||||||
|
create(path: string, content: Buffer | string): void {
|
||||||
|
return this._base.create(this._fullPath(path), content);
|
||||||
|
}
|
||||||
|
delete(path: string): void { return this._base.delete(this._fullPath(path)); }
|
||||||
|
rename(from: string, to: string): void {
|
||||||
|
return this._base.rename(this._fullPath(from), this._fullPath(to));
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(action: Action, strategy?: MergeStrategy): void {
|
||||||
|
return this._base.apply(action, strategy);
|
||||||
|
}
|
||||||
|
get actions(): Action[] { return this._base.actions; }
|
||||||
|
|
||||||
|
[TreeSymbol]() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fullPath(path: string) {
|
||||||
|
return join(this._root.scope, normalize('/' + path));
|
||||||
|
}
|
||||||
|
}
|
141
packages/angular_devkit/schematics/src/tree/scoped_spec.ts
Normal file
141
packages/angular_devkit/schematics/src/tree/scoped_spec.ts
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/**
|
||||||
|
* @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 { UnitTestTree } from '../../testing';
|
||||||
|
import { HostTree } from './host-tree';
|
||||||
|
import { ScopedTree } from './scoped';
|
||||||
|
|
||||||
|
|
||||||
|
describe('ScopedTree', () => {
|
||||||
|
let base: HostTree;
|
||||||
|
let scoped: ScopedTree;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
base = new HostTree();
|
||||||
|
base.create('/file-0-1', '0-1');
|
||||||
|
base.create('/file-0-2', '0-2');
|
||||||
|
base.create('/file-0-3', '0-3');
|
||||||
|
base.create('/level-1/file-1-1', '1-1');
|
||||||
|
base.create('/level-1/file-1-2', '1-2');
|
||||||
|
base.create('/level-1/file-1-3', '1-3');
|
||||||
|
base.create('/level-1/level-2/file-2-1', '2-1');
|
||||||
|
base.create('/level-1/level-2/file-2-2', '2-2');
|
||||||
|
base.create('/level-1/level-2/file-2-3', '2-3');
|
||||||
|
|
||||||
|
scoped = new ScopedTree(base, 'level-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports exists', () => {
|
||||||
|
expect(scoped.exists('/file-1-1')).toBeTruthy();
|
||||||
|
expect(scoped.exists('file-1-1')).toBeTruthy();
|
||||||
|
expect(scoped.exists('/level-2/file-2-1')).toBeTruthy();
|
||||||
|
expect(scoped.exists('level-2/file-2-1')).toBeTruthy();
|
||||||
|
|
||||||
|
expect(scoped.exists('/file-1-4')).toBeFalsy();
|
||||||
|
expect(scoped.exists('file-1-4')).toBeFalsy();
|
||||||
|
|
||||||
|
expect(scoped.exists('/file-0-1')).toBeFalsy();
|
||||||
|
expect(scoped.exists('file-0-1')).toBeFalsy();
|
||||||
|
expect(scoped.exists('/level-1/file-1-1')).toBeFalsy();
|
||||||
|
expect(scoped.exists('level-1/file-1-1')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports read', () => {
|
||||||
|
expect(scoped.read('/file-1-2')).not.toBeNull();
|
||||||
|
expect(scoped.read('file-1-2')).not.toBeNull();
|
||||||
|
|
||||||
|
const test = new UnitTestTree(scoped);
|
||||||
|
expect(test.readContent('/file-1-2')).toBe('1-2');
|
||||||
|
expect(test.readContent('file-1-2')).toBe('1-2');
|
||||||
|
|
||||||
|
expect(scoped.read('/file-0-2')).toBeNull();
|
||||||
|
expect(scoped.read('file-0-2')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports create', () => {
|
||||||
|
expect(() => scoped.create('/file-1-4', '1-4')).not.toThrow();
|
||||||
|
|
||||||
|
const test = new UnitTestTree(scoped);
|
||||||
|
expect(test.readContent('/file-1-4')).toBe('1-4');
|
||||||
|
expect(test.readContent('file-1-4')).toBe('1-4');
|
||||||
|
|
||||||
|
expect(base.exists('/level-1/file-1-4')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports delete', () => {
|
||||||
|
expect(() => scoped.delete('/file-0-3')).toThrow();
|
||||||
|
|
||||||
|
expect(() => scoped.delete('/file-1-3')).not.toThrow();
|
||||||
|
expect(scoped.exists('/file-1-3')).toBeFalsy();
|
||||||
|
|
||||||
|
expect(base.exists('/level-1/file-1-3')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports overwrite', () => {
|
||||||
|
expect(() => scoped.overwrite('/file-1-1', '1-1*')).not.toThrow();
|
||||||
|
expect(() => scoped.overwrite('/file-1-4', '1-4*')).toThrow();
|
||||||
|
|
||||||
|
const test = new UnitTestTree(scoped);
|
||||||
|
expect(test.readContent('/file-1-1')).toBe('1-1*');
|
||||||
|
expect(test.readContent('file-1-1')).toBe('1-1*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports rename', () => {
|
||||||
|
expect(() => scoped.rename('/file-1-1', '/file-1-1-new')).not.toThrow();
|
||||||
|
expect(() => scoped.rename('/file-1-4', '/file-1-4-new')).toThrow();
|
||||||
|
|
||||||
|
const test = new UnitTestTree(scoped);
|
||||||
|
expect(test.readContent('/file-1-1-new')).toBe('1-1');
|
||||||
|
expect(test.readContent('file-1-1-new')).toBe('1-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports get', () => {
|
||||||
|
expect(scoped.get('/file-1-1')).not.toBeNull();
|
||||||
|
|
||||||
|
const file = scoped.get('file-1-1');
|
||||||
|
expect(file && file.path as string).toBe('/file-1-1');
|
||||||
|
|
||||||
|
expect(scoped.get('/file-0-1')).toBeNull();
|
||||||
|
expect(scoped.get('file-0-1')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports getDir', () => {
|
||||||
|
expect(scoped.getDir('/level-2')).not.toBeNull();
|
||||||
|
|
||||||
|
const dir = scoped.getDir('level-2');
|
||||||
|
expect(dir.path as string).toBe('/level-2');
|
||||||
|
expect(dir.parent).not.toBeNull();
|
||||||
|
const files: string[] = [];
|
||||||
|
dir.visit(path => files.push(path));
|
||||||
|
files.sort();
|
||||||
|
expect(files).toEqual([
|
||||||
|
'/level-2/file-2-1',
|
||||||
|
'/level-2/file-2-2',
|
||||||
|
'/level-2/file-2-3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports visit', () => {
|
||||||
|
const files: string[] = [];
|
||||||
|
scoped.visit(path => files.push(path));
|
||||||
|
files.sort();
|
||||||
|
expect(files).toEqual([
|
||||||
|
'/file-1-1',
|
||||||
|
'/file-1-2',
|
||||||
|
'/file-1-3',
|
||||||
|
'/level-2/file-2-1',
|
||||||
|
'/level-2/file-2-2',
|
||||||
|
'/level-2/file-2-3',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports root', () => {
|
||||||
|
expect(scoped.root).not.toBeNull();
|
||||||
|
expect(scoped.root.path as string).toBe('/');
|
||||||
|
expect(scoped.root.parent).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user