Skip to content

Commit f3e389b

Browse files
hanslalexeagle
authored andcommitted
feat(@angular-devkit/schematics): add .template as an extension
New rules to deal with templates using a .template extension. Apply the template only to those files, then remove the .template suffix. Also added a new rename() rule that takes a matcher and a renamer. Nothing big there. Also added a new composeFileOperator() that compose operators one after the other.
1 parent 9aadb8e commit f3e389b

File tree

6 files changed

+182
-2
lines changed

6 files changed

+182
-2
lines changed

packages/angular_devkit/schematics/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ ts_library(
9999
),
100100
deps = [
101101
":schematics",
102+
":testing",
102103
"//packages/angular_devkit/core",
103104
"@rxjs",
104105
"@rxjs//operators",

packages/angular_devkit/schematics/src/rules/base.ts

+17
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,20 @@ export function forEach(operator: FileOperator): Rule {
185185
return tree;
186186
};
187187
}
188+
189+
190+
export function composeFileOperators(operators: FileOperator[]): FileOperator {
191+
return (entry: FileEntry) => {
192+
let current: FileEntry | null = entry;
193+
for (const op of operators) {
194+
current = op(current);
195+
196+
if (current === null) {
197+
// Deleted, just return.
198+
return null;
199+
}
200+
}
201+
202+
return current;
203+
};
204+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { normalize } from '@angular-devkit/core';
9+
import { Rule } from '../engine/interface';
10+
import { FilePredicate } from '../tree/interface';
11+
import { forEach } from './base';
12+
13+
14+
export function rename(match: FilePredicate<boolean>, to: FilePredicate<string>): Rule {
15+
return forEach(entry => {
16+
if (match(entry.path, entry)) {
17+
return {
18+
content: entry.content,
19+
path: normalize(to(entry.path, entry)),
20+
};
21+
} else {
22+
return entry;
23+
}
24+
});
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
// tslint:disable:non-null-operator
9+
import { of as observableOf } from 'rxjs';
10+
import { SchematicContext } from '../engine/interface';
11+
import { HostTree } from '../tree/host-tree';
12+
import { callRule } from './call';
13+
import { rename } from './rename';
14+
15+
16+
const context: SchematicContext = null !;
17+
18+
19+
describe('rename', () => {
20+
it('works', done => {
21+
const tree = new HostTree();
22+
tree.create('a/b/file1', 'hello world');
23+
tree.create('a/b/file2', 'hello world');
24+
tree.create('a/c/file3', 'hello world');
25+
26+
let i = 0;
27+
28+
// Rename all files that contain 'b' to 'hello'.
29+
callRule(rename(x => !!x.match(/b/), () => 'hello' + (i++)), observableOf(tree), context)
30+
.toPromise()
31+
.then(result => {
32+
expect(result.exists('a/b/file1')).toBe(false);
33+
expect(result.exists('a/b/file2')).toBe(false);
34+
expect(result.exists('hello0')).toBe(true);
35+
expect(result.exists('hello1')).toBe(true);
36+
expect(result.exists('a/c/file3')).toBe(true);
37+
})
38+
.then(done, done.fail);
39+
});
40+
41+
it('works (2)', done => {
42+
const tree = new HostTree();
43+
tree.create('a/b/file1', 'hello world');
44+
tree.create('a/b/file2', 'hello world');
45+
tree.create('a/c/file3', 'hello world');
46+
47+
let i = 0;
48+
49+
// Rename all files that contain 'b' to 'hello'.
50+
callRule(rename(x => !!x.match(/b/), x => x + (i++)), observableOf(tree), context)
51+
.toPromise()
52+
.then(result => {
53+
expect(result.exists('a/b/file1')).toBe(false);
54+
expect(result.exists('a/b/file2')).toBe(false);
55+
expect(result.exists('a/b/file10')).toBe(true);
56+
expect(result.exists('a/b/file21')).toBe(true);
57+
expect(result.exists('a/c/file3')).toBe(true);
58+
})
59+
.then(done, done.fail);
60+
});
61+
});

packages/angular_devkit/schematics/src/rules/template.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
import { BaseException, normalize, template as templateImpl } from '@angular-devkit/core';
99
import { FileOperator, Rule } from '../engine/interface';
1010
import { FileEntry } from '../tree/interface';
11-
import { chain, forEach } from './base';
11+
import { chain, composeFileOperators, forEach, when } from './base';
12+
import { rename } from './rename';
1213
import { isBinary } from './utils/is-binary';
1314

1415

16+
export const TEMPLATE_FILENAME_RE = /\.template$/;
17+
18+
1519
export class OptionIsNotDefinedException extends BaseException {
1620
constructor(name: string) { super(`Option "${name}" is not defined.`); }
1721
}
@@ -144,6 +148,17 @@ export function pathTemplate<T extends PathTemplateData>(options: T): Rule {
144148
}
145149

146150

151+
/**
152+
* Remove every `.template` suffix from file names.
153+
*/
154+
export function renameTemplateFiles(): Rule {
155+
return rename(
156+
path => !!path.match(TEMPLATE_FILENAME_RE),
157+
path => path.replace(TEMPLATE_FILENAME_RE, ''),
158+
);
159+
}
160+
161+
147162
export function template<T>(options: T): Rule {
148163
return chain([
149164
contentTemplate(options),
@@ -153,3 +168,23 @@ export function template<T>(options: T): Rule {
153168
pathTemplate(options as {} as PathTemplateData),
154169
]);
155170
}
171+
172+
173+
export function applyTemplates<T>(options: T): Rule {
174+
return forEach(
175+
when(
176+
path => path.endsWith('.template'),
177+
composeFileOperators([
178+
applyContentTemplate(options),
179+
// See above for this weird cast.
180+
applyPathTemplate(options as {} as PathTemplateData),
181+
entry => {
182+
return {
183+
content: entry.content,
184+
path: entry.path.replace(TEMPLATE_FILENAME_RE, ''),
185+
} as FileEntry;
186+
},
187+
]),
188+
),
189+
);
190+
}

packages/angular_devkit/schematics/src/rules/template_spec.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
// tslint:disable:no-implicit-dependencies
89
import { normalize } from '@angular-devkit/core';
9-
import { FileEntry } from '../tree/interface';
10+
import { UnitTestTree } from '@angular-devkit/schematics/testing';
11+
import { of as observableOf } from 'rxjs';
12+
import { SchematicContext } from '../engine/interface';
13+
import { HostTree } from '../tree/host-tree';
14+
import { FileEntry, MergeStrategy } from '../tree/interface';
15+
import { callRule } from './call';
1016
import {
1117
InvalidPipeException,
1218
OptionIsNotDefinedException,
1319
UnknownPipeException,
1420
applyContentTemplate,
1521
applyPathTemplate,
22+
applyTemplates,
1623
} from './template';
1724

1825

@@ -151,3 +158,37 @@ describe('contentTemplate', () => {
151158
expect(_applyContentTemplate('a<%= "\\n" + "b" %>b', {})).toBe('a\nbb');
152159
});
153160
});
161+
162+
163+
describe('applyTemplateFiles', () => {
164+
it('works with template files exclusively', done => {
165+
const tree = new UnitTestTree(new HostTree());
166+
tree.create('a/b/file1', 'hello world');
167+
tree.create('a/b/file2', 'hello world');
168+
tree.create('a/b/file3.template', 'hello <%= 1 %> world');
169+
tree.create('a/b/file__a__.template', 'hello <%= 1 %> world');
170+
tree.create('a/b/file__norename__', 'hello <%= 1 %> world');
171+
tree.create('a/c/file4', 'hello world');
172+
173+
const context: SchematicContext = {
174+
strategy: MergeStrategy.Default,
175+
} as SchematicContext;
176+
177+
// Rename all files that contain 'b' to 'hello'.
178+
callRule(applyTemplates({ a: 'foo' }), observableOf(tree), context)
179+
.toPromise()
180+
.then(() => {
181+
expect([...tree.files].sort()).toEqual([
182+
'/a/b/file1',
183+
'/a/b/file2',
184+
'/a/b/file3',
185+
'/a/b/file__norename__',
186+
'/a/b/filefoo',
187+
'/a/c/file4',
188+
]);
189+
190+
expect(tree.readContent('/a/b/file3')).toBe('hello 1 world');
191+
})
192+
.then(done, done.fail);
193+
});
194+
});

0 commit comments

Comments
 (0)