Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/schematics/angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ ts_library(
# Also exclude templated files.
"*/files/**/*.ts",
"*/other-files/**/*.ts",
"*/implements-files/**/*",
"*/type-files/**/*",
"*/functional-files/**/*",
"*/class-files/**/*",
# Exclude test helpers.
"utility/test/**/*.ts",
# NB: we need to exclude the nested node_modules that is laid out by yarn workspaces
Expand All @@ -65,6 +69,10 @@ ts_library(
"*/schema.json",
"*/files/**/*",
"*/other-files/**/*",
"*/implements-files/**/*",
"*/type-files/**/*",
"*/functional-files/**/*",
"*/class-files/**/*",
],
exclude = [
# NB: we need to exclude the nested node_modules that is laid out by yarn workspaces
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { <%= implementationImports %> } from '@angular/router';
import { <%= routerImports %> } from '@angular/router';
import { Observable } from 'rxjs';

@Injectable({
Expand Down
58 changes: 37 additions & 21 deletions packages/schematics/angular/guard/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,55 @@
*/

import { Rule, SchematicsException } from '@angular-devkit/schematics';

import { generateFromFiles } from '../utility/generate-from-files';

import { Implement as GuardInterface, Schema as GuardOptions } from './schema';

export default function (options: GuardOptions): Rule {
if (!options.implements) {
throw new SchematicsException('Option "implements" is required.');
}
if (options.implements.length > 1 && options.functional) {
throw new SchematicsException(
'Can only specify one value for implements when generating a functional guard.',
);
}

const implementations = options.implements
.map((implement) => (implement === 'CanDeactivate' ? 'CanDeactivate<unknown>' : implement))
.join(', ');
const commonRouterNameImports = ['ActivatedRouteSnapshot', 'RouterStateSnapshot'];
const routerNamedImports: string[] = [...options.implements, 'UrlTree'];

if (
options.implements.includes(GuardInterface.CanLoad) ||
options.implements.includes(GuardInterface.CanMatch)
) {
routerNamedImports.push('Route', 'UrlSegment');
if (options.functional) {
const guardType = options.implements[0] + 'Fn';

if (options.implements.length > 1) {
return generateFromFiles({ ...options, templateFilesDirectory: './type-files' }, { guardType });
} else {
const implementations = options.implements
.map((implement) => (implement === 'CanDeactivate' ? 'CanDeactivate<unknown>' : implement))
.join(', ');
const commonRouterNameImports = ['ActivatedRouteSnapshot', 'RouterStateSnapshot'];
const routerNamedImports: string[] = [...options.implements, 'UrlTree'];

if (
options.implements.includes(GuardInterface.CanLoad) ||
options.implements.includes(GuardInterface.CanMatch)
) {
routerNamedImports.push('Route', 'UrlSegment');

if (options.implements.length > 1) {
routerNamedImports.push(...commonRouterNameImports);
}
} else {
routerNamedImports.push(...commonRouterNameImports);
}
} else {
routerNamedImports.push(...commonRouterNameImports);
}

routerNamedImports.sort();
routerNamedImports.sort();

const implementationImports = routerNamedImports.join(', ');
const routerImports = routerNamedImports.join(', ');

return generateFromFiles(options, {
implementations,
implementationImports,
});
return generateFromFiles(
{ ...options, templateFilesDirectory: './implements-files' },
{
implementations,
routerImports,
},
);
}
}
63 changes: 51 additions & 12 deletions packages/schematics/angular/guard/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
*/

import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';

import { Schema as ApplicationOptions } from '../application/schema';
import { Schema as WorkspaceOptions } from '../workspace/schema';

import { Schema as GuardOptions } from './schema';

describe('Guard Schematic', () => {
Expand Down Expand Up @@ -90,6 +92,37 @@ describe('Guard Schematic', () => {
expect(fileString).not.toContain('canLoad');
});

it('should respect the functional guard value', async () => {
const options = { ...defaultOptions, implements: ['CanActivate'], functional: true };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
expect(fileString).toContain('export const fooGuard: CanActivateFn = (route, state) => {');
expect(fileString).not.toContain('CanActivateChild');
expect(fileString).not.toContain('canActivateChild');
expect(fileString).not.toContain('CanLoad');
expect(fileString).not.toContain('canLoad');
});

it('should generate a helper function to execute the guard in a test', async () => {
const options = { ...defaultOptions, implements: ['CanActivate'], functional: true };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.spec.ts');
expect(fileString).toContain('const executeGuard: CanActivateFn = (...guardParameters) => ');
expect(fileString).toContain(
'TestBed.inject(EnvironmentInjector).runInContext(() => fooGuard(...guardParameters));',
);
});

it('should generate CanDeactivateFn with unknown functional guard', async () => {
const options = { ...defaultOptions, implements: ['CanDeactivate'], functional: true };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
expect(fileString).toContain(
'export const fooGuard: CanDeactivateFn<unknown> = ' +
'(component, currentRoute, currentState, nextState) => {',
);
});

it('should respect the implements values', async () => {
const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand All @@ -104,18 +137,6 @@ describe('Guard Schematic', () => {
});
});

it('should use CanActivate if no implements value', async () => {
const options = { ...defaultOptions, implements: undefined };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
expect(fileString).toContain('CanActivate');
expect(fileString).toContain('canActivate');
expect(fileString).not.toContain('CanActivateChild');
expect(fileString).not.toContain('canActivateChild');
expect(fileString).not.toContain('CanLoad');
expect(fileString).not.toContain('canLoad');
});

it('should add correct imports based on CanLoad implementation', async () => {
const implementationOptions = ['CanLoad'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand All @@ -136,6 +157,15 @@ describe('Guard Schematic', () => {
expect(fileString).toContain(expectedImports);
});

it('should add correct imports based on canLoad functional guard', async () => {
const options = { ...defaultOptions, implements: ['CanLoad'], functional: true };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
const expectedImports = `import { CanLoadFn } from '@angular/router';`;

expect(fileString).toContain(expectedImports);
});

it('should add correct imports based on CanActivate implementation', async () => {
const implementationOptions = ['CanActivate'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand All @@ -146,6 +176,15 @@ describe('Guard Schematic', () => {
expect(fileString).toContain(expectedImports);
});

it('should add correct imports based on canActivate functional guard', async () => {
const options = { ...defaultOptions, implements: ['CanActivate'], functional: true };
const tree = await schematicRunner.runSchematicAsync('guard', options, appTree).toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.guard.ts');
const expectedImports = `import { CanActivateFn } from '@angular/router';`;

expect(fileString).toContain(expectedImports);
});

it('should add correct imports if multiple implementations was selected', async () => {
const implementationOptions = ['CanActivate', 'CanLoad', 'CanActivateChild'];
const options = { ...defaultOptions, implements: implementationOptions };
Expand Down
9 changes: 7 additions & 2 deletions packages/schematics/angular/guard/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,22 @@
"$source": "projectName"
}
},
"functional": {
"type": "boolean",
"description": "Specifies whether to generate a guard as a function.",
"default": false
},
"implements": {
"type": "array",
"description": "Specifies which interfaces to implement.",
"description": "Specifies which type of guard to create.",
"uniqueItems": true,
"minItems": 1,
"items": {
"enum": ["CanActivate", "CanActivateChild", "CanDeactivate", "CanLoad", "CanMatch"],
"type": "string"
Comment thread
atscott marked this conversation as resolved.
},
"default": ["CanActivate"],
Comment thread
atscott marked this conversation as resolved.
"x-prompt": "Which interfaces would you like to implement?"
"x-prompt": "Which type of guard would you like to create?"
}
},
"required": ["name", "project"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { EnvironmentInjector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { <%= guardType %> } from '@angular/router';

import { <%= camelize(name) %>Guard } from './<%= dasherize(name) %>.guard';

describe('<%= camelize(name) %>Guard', () => {
const executeGuard: <%= guardType %> = (...guardParameters) =>
TestBed.inject(EnvironmentInjector).runInContext(() => <%= camelize(name) %>Guard(...guardParameters));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(<%= camelize(name) %>Guard).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { <%= guardType %> } from '@angular/router';

export const <%= camelize(name) %>Guard: <%= guardType %><% if (guardType === 'CanDeactivateFn') { %><unknown><% } %> = <%
if (guardType === 'CanMatchFn' || guardType === 'CanLoadFn') { %>(route, segments)<% }
%><% if (guardType === 'CanActivateFn') { %>(route, state)<% }
%><% if (guardType === 'CanActivateChildFn') { %>(childRoute, state)<% }
%><% if (guardType === 'CanDeactivateFn') { %>(component, currentRoute, currentState, nextState)<% } %> => {
return true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';

import { <%= camelize(name) %>Resolver } from './<%= dasherize(name) %>.resolver';

describe('<%= camelize(name) %>Resolver', () => {
const executeResolver: ResolveFn<boolean> = (...resolverParameters) =>
TestBed.inject(EnvironmentInjector).runInContext(() => <%= camelize(name) %>Resolver(...resolverParameters));

beforeEach(() => {
TestBed.configureTestingModule({});
});

it('should be created', () => {
expect(resolver).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ResolveFn } from '@angular/router';

export const <%= camelize(name) %>Resolver: ResolveFn<boolean> = (route, state) => {
return true;
}
4 changes: 3 additions & 1 deletion packages/schematics/angular/resolver/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ import { generateFromFiles } from '../utility/generate-from-files';
import { Schema } from './schema';

export default function (options: Schema): Rule {
return generateFromFiles(options);
return options.functional
? generateFromFiles({ ...options, templateFilesDirectory: './functional-files' })
: generateFromFiles({ ...options, templateFilesDirectory: './class-files' });
}
23 changes: 23 additions & 0 deletions packages/schematics/angular/resolver/index_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,27 @@ describe('resolver Schematic', () => {
.toPromise();
expect(appTree.files).toContain('/projects/bar/custom/app/foo.resolver.ts');
});

it('should create a functional resolver', async () => {
const tree = await schematicRunner
.runSchematicAsync('resolver', { ...defaultOptions, functional: true }, appTree)
.toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.resolver.ts');
expect(fileString).toContain(
'export const fooResolver: ResolveFn<boolean> = (route, state) => {',
);
});

it('should create a helper function to run a functional resolver in a test', async () => {
const tree = await schematicRunner
.runSchematicAsync('resolver', { ...defaultOptions, functional: true }, appTree)
.toPromise();
const fileString = tree.readContent('/projects/bar/src/app/foo.resolver.spec.ts');
expect(fileString).toContain(
'const executeResolver: ResolveFn<boolean> = (...resolverParameters) => ',
);
expect(fileString).toContain(
'TestBed.inject(EnvironmentInjector).runInContext(() => fooResolver(...resolverParameters));',
);
});
});
5 changes: 5 additions & 0 deletions packages/schematics/angular/resolver/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
"description": "When true (the default), creates the new files at the top level of the current project.",
"default": true
},
"functional": {
"type": "boolean",
"description": "Creates the resolver as a `ResolveFn`.",
"default": false
},
"path": {
"type": "string",
"format": "path",
Expand Down
4 changes: 3 additions & 1 deletion packages/schematics/angular/utility/generate-from-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface GenerateFromFilesOptions {
prefix?: string;
project: string;
skipTests?: boolean;
templateFilesDirectory?: string;
}

export function generateFromFiles(
Expand All @@ -47,7 +48,8 @@ export function generateFromFiles(

validateClassName(strings.classify(options.name));

const templateSource = apply(url('./files'), [
const templateFilesDirectory = options.templateFilesDirectory ?? './files';
const templateSource = apply(url(templateFilesDirectory), [
options.skipTests ? filter((path) => !path.endsWith('.spec.ts.template')) : noop(),
applyTemplates({
...strings,
Expand Down