mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-18 03:23:57 +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 {
|
||||
interactive: boolean;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
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 {
|
||||
scope: string;
|
||||
interactive: boolean;
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import { Observable, of as observableOf } from 'rxjs';
|
||||
import { concatMap, first, map } from 'rxjs/operators';
|
||||
import { callRule } from '../rules/call';
|
||||
import { Tree } from '../tree/interface';
|
||||
import { ScopedTree } from '../tree/scoped';
|
||||
import {
|
||||
Collection,
|
||||
Engine,
|
||||
@ -58,7 +59,28 @@ export class SchematicImpl<CollectionT extends object, SchematicT extends object
|
||||
map(o => [tree, o]),
|
||||
)),
|
||||
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);
|
||||
});
|
||||
|
||||
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