From f3e389bd8c6959a2201cbe35ce640de477a350a0 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Fri, 20 Jul 2018 11:32:20 -0700 Subject: [PATCH] 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. --- packages/angular_devkit/schematics/BUILD | 1 + .../schematics/src/rules/base.ts | 17 ++++++ .../schematics/src/rules/rename.ts | 25 ++++++++ .../schematics/src/rules/rename_spec.ts | 61 +++++++++++++++++++ .../schematics/src/rules/template.ts | 37 ++++++++++- .../schematics/src/rules/template_spec.ts | 43 ++++++++++++- 6 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 packages/angular_devkit/schematics/src/rules/rename.ts create mode 100644 packages/angular_devkit/schematics/src/rules/rename_spec.ts diff --git a/packages/angular_devkit/schematics/BUILD b/packages/angular_devkit/schematics/BUILD index 629b6671f9..0d953c153a 100644 --- a/packages/angular_devkit/schematics/BUILD +++ b/packages/angular_devkit/schematics/BUILD @@ -99,6 +99,7 @@ ts_library( ), deps = [ ":schematics", + ":testing", "//packages/angular_devkit/core", "@rxjs", "@rxjs//operators", diff --git a/packages/angular_devkit/schematics/src/rules/base.ts b/packages/angular_devkit/schematics/src/rules/base.ts index 94a041a1ae..ee2744fffa 100644 --- a/packages/angular_devkit/schematics/src/rules/base.ts +++ b/packages/angular_devkit/schematics/src/rules/base.ts @@ -185,3 +185,20 @@ export function forEach(operator: FileOperator): Rule { 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; + }; +} diff --git a/packages/angular_devkit/schematics/src/rules/rename.ts b/packages/angular_devkit/schematics/src/rules/rename.ts new file mode 100644 index 0000000000..2405d53022 --- /dev/null +++ b/packages/angular_devkit/schematics/src/rules/rename.ts @@ -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, to: FilePredicate): Rule { + return forEach(entry => { + if (match(entry.path, entry)) { + return { + content: entry.content, + path: normalize(to(entry.path, entry)), + }; + } else { + return entry; + } + }); +} diff --git a/packages/angular_devkit/schematics/src/rules/rename_spec.ts b/packages/angular_devkit/schematics/src/rules/rename_spec.ts new file mode 100644 index 0000000000..8cc869dab7 --- /dev/null +++ b/packages/angular_devkit/schematics/src/rules/rename_spec.ts @@ -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); + }); +}); diff --git a/packages/angular_devkit/schematics/src/rules/template.ts b/packages/angular_devkit/schematics/src/rules/template.ts index a39bb0f551..3e5d34e924 100644 --- a/packages/angular_devkit/schematics/src/rules/template.ts +++ b/packages/angular_devkit/schematics/src/rules/template.ts @@ -8,10 +8,14 @@ import { BaseException, normalize, template as templateImpl } from '@angular-devkit/core'; import { FileOperator, Rule } from '../engine/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'; +export const TEMPLATE_FILENAME_RE = /\.template$/; + + export class OptionIsNotDefinedException extends BaseException { constructor(name: string) { super(`Option "${name}" is not defined.`); } } @@ -144,6 +148,17 @@ export function pathTemplate(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(options: T): Rule { return chain([ contentTemplate(options), @@ -153,3 +168,23 @@ export function template(options: T): Rule { pathTemplate(options as {} as PathTemplateData), ]); } + + +export function applyTemplates(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; + }, + ]), + ), + ); +} diff --git a/packages/angular_devkit/schematics/src/rules/template_spec.ts b/packages/angular_devkit/schematics/src/rules/template_spec.ts index 2c30c81713..56e99f9a90 100644 --- a/packages/angular_devkit/schematics/src/rules/template_spec.ts +++ b/packages/angular_devkit/schematics/src/rules/template_spec.ts @@ -5,14 +5,21 @@ * 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:no-implicit-dependencies 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 { InvalidPipeException, OptionIsNotDefinedException, UnknownPipeException, applyContentTemplate, applyPathTemplate, + applyTemplates, } from './template'; @@ -151,3 +158,37 @@ describe('contentTemplate', () => { 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); + }); +});