Skip to content
This repository has been archived by the owner on Dec 8, 2022. It is now read-only.

Add support for route guards #168

Merged
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
43 changes: 41 additions & 2 deletions e2e/shared/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function prepareBuild(config) {
resetConfig();

// Create our server
httpServer = HttpServer.createServer({ root: tmp });
httpServer = HttpServer.createServer({ root: tmp, cache: 0 });

return new Promise((resolve, reject) => {
portfinder.getPortPromise()
Expand Down Expand Up @@ -193,6 +193,43 @@ function writeConfigServe(port) {
});
}

/**
* Write a file into the src/app folder -- Used for injecting files prior to build
* that we don't want to include in the skyux-template but need to test
*/
function writeAppFile(filePath, content) {
return new Promise((resolve, reject) => {
const resolvedFilePath = path.join(path.resolve(tmp), 'src', 'app', filePath);
fs.writeFile(resolvedFilePath, content, (err) => {
if (err) {
reject(err);
return;
}

resolve();
});
});
}

/**
* Remove file from the src/app folder -- Used for cleaning up after we've injected
* files for a specific test or group of tests
*/
function removeAppFile(filePath) {
return new Promise((resolve, reject) => {
const resolvedFilePath = path.join(path.resolve(tmp), 'src', 'app', filePath);

fs.unlink(resolvedFilePath, (err) => {
if (err) {
reject(err);
return;
}

resolve();
});
});
}

module.exports = {
afterAll: afterAll,
catchReject: catchReject,
Expand All @@ -203,5 +240,7 @@ module.exports = {
getExitCode: getExitCode,
prepareBuild: prepareBuild,
prepareServe: prepareServe,
tmp: tmp
tmp: tmp,
writeAppFile: writeAppFile,
removeAppFile: removeAppFile
};
7 changes: 7 additions & 0 deletions e2e/shared/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,12 @@ module.exports = {
nav.get(1).click();
expect(element(by.tagName('h1')).getText()).toBe('About our Team');
done();
},

respectGuardCanActivate: (done) => {
const nav = $$('.sky-navbar-item a');
nav.get(1).click();
expect(element(by.tagName('h1')).getText()).toBe('SKY UX Template');
done();
}
};
59 changes: 43 additions & 16 deletions e2e/skyux-build-aot.e2e-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,55 @@
const common = require('./shared/common');
const tests = require('./shared/tests');

describe('skyux build aot', () => {
function prepareBuild() {
const opts = { mode: 'easy', name: 'dist', compileMode: 'aot' };
return common.prepareBuild(opts)
.catch(console.error);
}

beforeAll((done) => {
const opts = { mode: 'easy', name: 'dist', compileMode: 'aot' };
common.prepareBuild(opts)
.then(done)
.catch(err => {
console.log(err);
done();
});
});
describe('skyux build aot', () => {
describe('w/base template', () => {
beforeAll((done) => prepareBuild().then(done));

afterAll(common.afterAll);
it('should have exitCode 0', tests.verifyExitCode);

it('should have exitCode 0', tests.verifyExitCode);
it('should generate expected static files', tests.verifyFiles);

it('should generate expected static files', tests.verifyFiles);
it('should render the home components', tests.renderHomeComponent);

it('should render the home components', tests.renderHomeComponent);
it('should render shared nav component', tests.renderSharedNavComponent);

it('should render shared nav component', tests.renderSharedNavComponent);
it('should follow routerLink and render about component', tests.followRouterLinkRenderAbout);

it('should follow routerLink and render about component', tests.followRouterLinkRenderAbout);
afterAll(common.afterAll);
});

describe('w/guard', () => {
beforeAll((done) => {
const guard = `
import { Injectable } from '@angular/core';
@Injectable()
export class AboutGuard {
canActivate(next: any, state: any) {
return false;
}
}
`;

common.writeAppFile('about/index.guard.ts', guard)
.then(() => prepareBuild())
.then(done)
.catch(console.error);
});

it('should not follow routerLink when guard returns false', tests.respectGuardCanActivate);

afterAll((done) => {
common.removeAppFile('about/index.guard.ts')
.then(() => common.afterAll())
.then(done)
.catch(console.error);
});
});
});
59 changes: 43 additions & 16 deletions e2e/skyux-build-jit.e2e-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,55 @@
const common = require('./shared/common');
const tests = require('./shared/tests');

describe('skyux build jit', () => {
function prepareBuild() {
const opts = { mode: 'easy', name: 'dist', compileMode: 'jit' };
return common.prepareBuild(opts)
.catch(err => console.error);
}

beforeAll((done) => {
const opts = { mode: 'easy', name: 'dist', compileMode: 'jit' };
common.prepareBuild(opts)
.then(done)
.catch(err => {
console.log(err);
done();
});
});
describe('skyux build jit', () => {
describe('w/base template', () => {
beforeAll((done) => prepareBuild().then(done));

afterAll(common.afterAll);
it('should have exitCode 0', tests.verifyExitCode);

it('should have exitCode 0', tests.verifyExitCode);
it('should generate expected static files', tests.verifyFiles);

it('should generate expected static files', tests.verifyFiles);
it('should render the home components', tests.renderHomeComponent);

it('should render the home components', tests.renderHomeComponent);
it('should render shared nav component', tests.renderSharedNavComponent);

it('should render shared nav component', tests.renderSharedNavComponent);
it('should follow routerLink and render about component', tests.followRouterLinkRenderAbout);

it('should follow routerLink and render about component', tests.followRouterLinkRenderAbout);
afterAll(common.afterAll);
});

describe('w/guard', () => {
beforeAll((done) => {
const guard = `
import { Injectable } from '@angular/core';

@Injectable()
export class AboutGuard {
canActivate(next: any, state: any) {
return false;
}
}
`;

common.writeAppFile('about/index.guard.ts', guard)
.then(() => prepareBuild())
.then(done)
.catch(console.error);
});

it('should not follow routerLink when guard returns false', tests.respectGuardCanActivate);

afterAll((done) => {
common.removeAppFile('about/index.guard.ts')
.then(() => common.afterAll())
.then(done)
.catch(console.error);
});
});
});
3 changes: 2 additions & 1 deletion lib/sky-pages-module-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ import {
AppExtrasModule
} from '${skyAppConfig.runtime.skyPagesOutAlias}/src/app/app-extras.module';
import { ${runtimeImports.join(', ')} } from '${skyAppConfig.runtime.runtimeAlias}';
${routes.imports.join('\n')}

export function SkyAppConfigFactory(windowRef: SkyAppWindowRef): any {
const config: any = ${skyAppConfigAsString};
Expand All @@ -141,7 +142,7 @@ ${components.imports}
${routes.definitions}

// Routes need to be defined after their corresponding components
const appRoutingProviders: any[] = [];
const appRoutingProviders: any[] = [${routes.providers}];
const routes: Routes = ${routes.declarations};
const routing = RouterModule.forRoot(routes);

Expand Down
57 changes: 55 additions & 2 deletions lib/sky-pages-route-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

const glob = require('glob');
const path = require('path');
const fs = require('fs');

function indent(count) {
return ' '.repeat(count);
Expand Down Expand Up @@ -53,6 +54,15 @@ function parseFileIntoEntity(skyAppConfig, file, index) {

let routePath = [];
let routeParams = [];
let parsedPath = path.parse(file);

// Make no assumptions on extension used to create route, just remove
// it and append .guard.ts (ex: index.html -> index.guard.ts)
let guardPath = path.join(parsedPath.dir, `${parsedPath.name}.guard.ts`);
let guard;
if (fs.existsSync(guardPath)) {
guard = extractGuard(guardPath);
}

// Removes srcPath + filename
// glob always uses '/' for path separator!
Expand Down Expand Up @@ -85,7 +95,8 @@ function parseFileIntoEntity(skyAppConfig, file, index) {
componentName: componentName,
componentDefinition: componentDefinition,
routePath: routePath.join('/'),
routeParams: routeParams
routeParams: routeParams,
guard: guard
};
}

Expand Down Expand Up @@ -114,11 +125,34 @@ function generateDefinitions(routes) {
function generateDeclarations(routes) {
const p = indent(1);
const declarations = routes
.map(r => `${p}{ path: '${r.routePath}', component: ${r.componentName} }`)
.map(r => {
let guard = r.guard ? r.guard.name : '';
let declaration =
`${p}{
path: '${r.routePath}',
component: ${r.componentName},
canActivate: [${guard}],
canDeactivate: [${guard}]
}`;

return declaration;
})
.join(',\n');
return `[\n${declarations}\n]`;
}

function generateRuntimeImports(routes) {
return routes
.filter(r => r.guard)
.map(r => `import { ${r.guard.name} } from '${r.guard.path.replace(/\.ts$/, '')}';`);
}

function generateProviders(routes) {
return routes
.filter(r => r.guard)
.map(r => r.guard.name);
}

function generateNames(routes) {
return routes.map(route => route.componentName);
}
Expand All @@ -137,11 +171,30 @@ function getRoutes(skyAppConfig) {
return {
declarations: generateDeclarations(routes),
definitions: generateDefinitions(routes),
imports: generateRuntimeImports(routes),
providers: generateProviders(routes),
names: generateNames(routes),
routesForConfig: getRoutesForConfig(routes)
};
}

function extractGuard(file) {
const matchRegexp = /@Injectable\s*\(\s*\)\s*export\s*class\s(\w+)/g;
const content = fs.readFileSync(file, { encoding: 'utf8' });

let result;
let match;
while ((match = matchRegexp.exec(content))) {
if (result !== undefined) {
throw new Error(`As a best practice, only export one guard per file in ${file}`);
}

result = { path: path.resolve(file), name: match[1] };
}

return result;
}

module.exports = {
getRoutes: getRoutes
};
41 changes: 41 additions & 0 deletions test/sky-pages-route-generator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,45 @@ describe('SKY UX Builder route generator', () => {
expect(suppliedPattern).toEqual('my-custom-src/my-custom-pattern');
});

it('should support guards with custom routesPattern', () => {
spyOn(glob, 'sync').and.callFake(() => ['my-custom-src/my-custom-route/index.html']);
spyOn(fs, 'readFileSync').and.returnValue('@Injectable() export class Guard {}');
spyOn(fs, 'existsSync').and.returnValue(true);

let routes = generator.getRoutes({
runtime: {
srcPath: 'my-custom-src/',
routesPattern: 'my-custom-pattern',
}
});

expect(routes.declarations).toContain(
`canActivate: [Guard]`
);

expect(routes.declarations).toContain(
`canDeactivate: [Guard]`
);

expect(routes.providers).toContain(
`Guard`
);
});

it('should throw when a file has multiple guards', () => {
spyOn(glob, 'sync').and.callFake(() => ['my-custom-src/my-custom-route/index.html']);
spyOn(fs, 'existsSync').and.returnValue(true);
spyOn(fs, 'readFileSync').and.returnValue(`
@Injectable() export class Guard {}
@Injectable() export class Guard2 {}
`);

let file = 'my-custom-src/my-custom-route/index.guard.ts';
expect(() => generator.getRoutes({
runtime: {
srcPath: 'my-custom-src/',
routesPattern: 'my-custom-pattern',
}
})).toThrow(new Error(`As a best practice, only export one guard per file in ${file}`));
});
});