mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-18 03:23:57 +08:00
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.
This commit is contained in:
parent
9aadb8e6d1
commit
f3e389bd8c
@ -99,6 +99,7 @@ ts_library(
|
|||||||
),
|
),
|
||||||
deps = [
|
deps = [
|
||||||
":schematics",
|
":schematics",
|
||||||
|
":testing",
|
||||||
"//packages/angular_devkit/core",
|
"//packages/angular_devkit/core",
|
||||||
"@rxjs",
|
"@rxjs",
|
||||||
"@rxjs//operators",
|
"@rxjs//operators",
|
||||||
|
@ -185,3 +185,20 @@ export function forEach(operator: FileOperator): Rule {
|
|||||||
return tree;
|
return tree;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function composeFileOperators(operators: FileOperator[]): FileOperator {
|
||||||
|
return (entry: FileEntry) => {
|
||||||
|
let current: FileEntry | null = entry;
|
||||||
|
for (const op of operators) {
|
||||||
|
current = op(current);
|
||||||
|
|
||||||
|
if (current === null) {
|
||||||
|
// Deleted, just return.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
25
packages/angular_devkit/schematics/src/rules/rename.ts
Normal file
25
packages/angular_devkit/schematics/src/rules/rename.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* @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 { normalize } from '@angular-devkit/core';
|
||||||
|
import { Rule } from '../engine/interface';
|
||||||
|
import { FilePredicate } from '../tree/interface';
|
||||||
|
import { forEach } from './base';
|
||||||
|
|
||||||
|
|
||||||
|
export function rename(match: FilePredicate<boolean>, to: FilePredicate<string>): Rule {
|
||||||
|
return forEach(entry => {
|
||||||
|
if (match(entry.path, entry)) {
|
||||||
|
return {
|
||||||
|
content: entry.content,
|
||||||
|
path: normalize(to(entry.path, entry)),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
61
packages/angular_devkit/schematics/src/rules/rename_spec.ts
Normal file
61
packages/angular_devkit/schematics/src/rules/rename_spec.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
// tslint:disable:non-null-operator
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { SchematicContext } from '../engine/interface';
|
||||||
|
import { HostTree } from '../tree/host-tree';
|
||||||
|
import { callRule } from './call';
|
||||||
|
import { rename } from './rename';
|
||||||
|
|
||||||
|
|
||||||
|
const context: SchematicContext = null !;
|
||||||
|
|
||||||
|
|
||||||
|
describe('rename', () => {
|
||||||
|
it('works', done => {
|
||||||
|
const tree = new HostTree();
|
||||||
|
tree.create('a/b/file1', 'hello world');
|
||||||
|
tree.create('a/b/file2', 'hello world');
|
||||||
|
tree.create('a/c/file3', 'hello world');
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
// Rename all files that contain 'b' to 'hello'.
|
||||||
|
callRule(rename(x => !!x.match(/b/), () => 'hello' + (i++)), observableOf(tree), context)
|
||||||
|
.toPromise()
|
||||||
|
.then(result => {
|
||||||
|
expect(result.exists('a/b/file1')).toBe(false);
|
||||||
|
expect(result.exists('a/b/file2')).toBe(false);
|
||||||
|
expect(result.exists('hello0')).toBe(true);
|
||||||
|
expect(result.exists('hello1')).toBe(true);
|
||||||
|
expect(result.exists('a/c/file3')).toBe(true);
|
||||||
|
})
|
||||||
|
.then(done, done.fail);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works (2)', done => {
|
||||||
|
const tree = new HostTree();
|
||||||
|
tree.create('a/b/file1', 'hello world');
|
||||||
|
tree.create('a/b/file2', 'hello world');
|
||||||
|
tree.create('a/c/file3', 'hello world');
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
// Rename all files that contain 'b' to 'hello'.
|
||||||
|
callRule(rename(x => !!x.match(/b/), x => x + (i++)), observableOf(tree), context)
|
||||||
|
.toPromise()
|
||||||
|
.then(result => {
|
||||||
|
expect(result.exists('a/b/file1')).toBe(false);
|
||||||
|
expect(result.exists('a/b/file2')).toBe(false);
|
||||||
|
expect(result.exists('a/b/file10')).toBe(true);
|
||||||
|
expect(result.exists('a/b/file21')).toBe(true);
|
||||||
|
expect(result.exists('a/c/file3')).toBe(true);
|
||||||
|
})
|
||||||
|
.then(done, done.fail);
|
||||||
|
});
|
||||||
|
});
|
@ -8,10 +8,14 @@
|
|||||||
import { BaseException, normalize, template as templateImpl } from '@angular-devkit/core';
|
import { BaseException, normalize, template as templateImpl } from '@angular-devkit/core';
|
||||||
import { FileOperator, Rule } from '../engine/interface';
|
import { FileOperator, Rule } from '../engine/interface';
|
||||||
import { FileEntry } from '../tree/interface';
|
import { FileEntry } from '../tree/interface';
|
||||||
import { chain, forEach } from './base';
|
import { chain, composeFileOperators, forEach, when } from './base';
|
||||||
|
import { rename } from './rename';
|
||||||
import { isBinary } from './utils/is-binary';
|
import { isBinary } from './utils/is-binary';
|
||||||
|
|
||||||
|
|
||||||
|
export const TEMPLATE_FILENAME_RE = /\.template$/;
|
||||||
|
|
||||||
|
|
||||||
export class OptionIsNotDefinedException extends BaseException {
|
export class OptionIsNotDefinedException extends BaseException {
|
||||||
constructor(name: string) { super(`Option "${name}" is not defined.`); }
|
constructor(name: string) { super(`Option "${name}" is not defined.`); }
|
||||||
}
|
}
|
||||||
@ -144,6 +148,17 @@ export function pathTemplate<T extends PathTemplateData>(options: T): Rule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove every `.template` suffix from file names.
|
||||||
|
*/
|
||||||
|
export function renameTemplateFiles(): Rule {
|
||||||
|
return rename(
|
||||||
|
path => !!path.match(TEMPLATE_FILENAME_RE),
|
||||||
|
path => path.replace(TEMPLATE_FILENAME_RE, ''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function template<T>(options: T): Rule {
|
export function template<T>(options: T): Rule {
|
||||||
return chain([
|
return chain([
|
||||||
contentTemplate(options),
|
contentTemplate(options),
|
||||||
@ -153,3 +168,23 @@ export function template<T>(options: T): Rule {
|
|||||||
pathTemplate(options as {} as PathTemplateData),
|
pathTemplate(options as {} as PathTemplateData),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function applyTemplates<T>(options: T): Rule {
|
||||||
|
return forEach(
|
||||||
|
when(
|
||||||
|
path => path.endsWith('.template'),
|
||||||
|
composeFileOperators([
|
||||||
|
applyContentTemplate(options),
|
||||||
|
// See above for this weird cast.
|
||||||
|
applyPathTemplate(options as {} as PathTemplateData),
|
||||||
|
entry => {
|
||||||
|
return {
|
||||||
|
content: entry.content,
|
||||||
|
path: entry.path.replace(TEMPLATE_FILENAME_RE, ''),
|
||||||
|
} as FileEntry;
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -5,14 +5,21 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* 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
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
// tslint:disable:no-implicit-dependencies
|
||||||
import { normalize } from '@angular-devkit/core';
|
import { normalize } from '@angular-devkit/core';
|
||||||
import { FileEntry } from '../tree/interface';
|
import { UnitTestTree } from '@angular-devkit/schematics/testing';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { SchematicContext } from '../engine/interface';
|
||||||
|
import { HostTree } from '../tree/host-tree';
|
||||||
|
import { FileEntry, MergeStrategy } from '../tree/interface';
|
||||||
|
import { callRule } from './call';
|
||||||
import {
|
import {
|
||||||
InvalidPipeException,
|
InvalidPipeException,
|
||||||
OptionIsNotDefinedException,
|
OptionIsNotDefinedException,
|
||||||
UnknownPipeException,
|
UnknownPipeException,
|
||||||
applyContentTemplate,
|
applyContentTemplate,
|
||||||
applyPathTemplate,
|
applyPathTemplate,
|
||||||
|
applyTemplates,
|
||||||
} from './template';
|
} from './template';
|
||||||
|
|
||||||
|
|
||||||
@ -151,3 +158,37 @@ describe('contentTemplate', () => {
|
|||||||
expect(_applyContentTemplate('a<%= "\\n" + "b" %>b', {})).toBe('a\nbb');
|
expect(_applyContentTemplate('a<%= "\\n" + "b" %>b', {})).toBe('a\nbb');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('applyTemplateFiles', () => {
|
||||||
|
it('works with template files exclusively', done => {
|
||||||
|
const tree = new UnitTestTree(new HostTree());
|
||||||
|
tree.create('a/b/file1', 'hello world');
|
||||||
|
tree.create('a/b/file2', 'hello world');
|
||||||
|
tree.create('a/b/file3.template', 'hello <%= 1 %> world');
|
||||||
|
tree.create('a/b/file__a__.template', 'hello <%= 1 %> world');
|
||||||
|
tree.create('a/b/file__norename__', 'hello <%= 1 %> world');
|
||||||
|
tree.create('a/c/file4', 'hello world');
|
||||||
|
|
||||||
|
const context: SchematicContext = {
|
||||||
|
strategy: MergeStrategy.Default,
|
||||||
|
} as SchematicContext;
|
||||||
|
|
||||||
|
// Rename all files that contain 'b' to 'hello'.
|
||||||
|
callRule(applyTemplates({ a: 'foo' }), observableOf(tree), context)
|
||||||
|
.toPromise()
|
||||||
|
.then(() => {
|
||||||
|
expect([...tree.files].sort()).toEqual([
|
||||||
|
'/a/b/file1',
|
||||||
|
'/a/b/file2',
|
||||||
|
'/a/b/file3',
|
||||||
|
'/a/b/file__norename__',
|
||||||
|
'/a/b/filefoo',
|
||||||
|
'/a/c/file4',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(tree.readContent('/a/b/file3')).toBe('hello 1 world');
|
||||||
|
})
|
||||||
|
.then(done, done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user