diff --git a/addon/ng2/blueprints/guard/files/__path__/__name__.service.ts b/addon/ng2/blueprints/guard/files/__path__/__name__.service.ts new file mode 100644 index 000000000000..5ee697accf58 --- /dev/null +++ b/addon/ng2/blueprints/guard/files/__path__/__name__.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { + Can<%= hook %>, + ActivatedRouteSnapShot, + RouterStateSnapShot, + Router +} from '@angular/router'; + +@Injectable() +export class <%= classifiedModuleName %> implements Can<%= hook %> { + constructor(private router: Router) {} + + can<%= hook %>(<%= route %>: ActivatedRouteSnapShot, state: RouterStateSnapShot) { + + } +} diff --git a/addon/ng2/blueprints/guard/index.js b/addon/ng2/blueprints/guard/index.js new file mode 100644 index 000000000000..d734b76292ae --- /dev/null +++ b/addon/ng2/blueprints/guard/index.js @@ -0,0 +1,99 @@ +const SilentError = require('silent-error'); +const path = require('path'); +const util = require('../../utilities/route-utils'); +var dynamicPathParser = require('../../utilities/dynamic-path-parser'); + +module.exports = { + description: 'Generate guard', + + availableOptions: [ + { name: 'can-activate', type: Boolean, default: false, aliases: ['ca'] }, + { name: 'can-deactivate', type: Boolean, default: false, aliases: ['cd'] }, + { name: 'can-activate-child', type: Boolean, default: false, aliases: ['cach'] }, + { name: 'route', type: String }, + { name: 'name', type: String } + ], + + normalizeEntityName: function(entityName) { + var parsedPath = dynamicPathParser(this.project, entityName); + this.dynamicPath = parsedPath; + this.guardPath = entityName; + this.entityName = path.parse(entityName).name.split('.')[0]; + return this.entityName; + }, + + locals: function(options) { + var activate = options.canActivate; + var deactivate = options.canDeactivate; + var activateChild = options.canActivateChild; + // make sure only one option is selected + if (activate + deactivate + activateChild !== 1) { + throw new SilentError('Please choose only one of the aliases ca, cd, cach'); + } + this.hook = activate ? 'Activate' : ( deactivate ? 'Deactivate' : 'ActivateChild'); + var customVariables = { + hook: this.hook, + route: activate || deactivate ? 'route' : 'childRoute' + }; + if (options.name) { + customVariables['classifiedModuleName'] = options.name; + } + return customVariables; + }, + + fileMapTokens: function() { + return { + __name__: () => this.entityName, + __path__: () => this.dynamicPath.dir + } + }, + + beforeInstall: function() { + this.importPath = this.guardPath[0] === path.sep ? + path.resolve(path.join(this.project.root, 'src', 'app', this.guardPath)) : + path.join(process.env.PWD, this.guardPath); + if (this.importPath.indexOf('src' + path.sep + 'app') === -1) { + throw new SilentError('Guard must be within app'); + } + + }, + // TODO: resolve import path and disallow duplicate guards + afterInstall: function(options) { + return this._locals(options) + .then(names => { + var mainFile = path.join(this.project.root, 'src', 'main.ts'); + var routesFile = path.join(this.project.root, 'src', 'routes.ts'); + var importPath = '.' + this.importPath.replace(path.join(this.project.root, 'src'), ''); + + var imports = {}; + var toInsert = {}; + var importedClass = names.classifiedModuleName; + imports[importedClass] = [ `${importPath}.service` ]; + var route = this.guardPath.split('/').filter(n => n !== '').join('/') // strip outer slashes + toInsert[route] = [ `can${this.hook}`, `[ ${importedClass} ]` ]; + var updateRoutesFile = util.addItemsToRouteProperties(routesFile, toInsert); + updateRoutesFile.concat(util.insertImport(routesFile, importedClass, importPath)); + + return util.applyChanges(updateRoutesFile) + .then(() => util.applyChanges(util.bootstrapItem(mainFile, imports, importedClass))); + }); + } +} + + +// .then(function() { +// return ng(['generate', 'guard', '/test-route', '--ca']) +// }).then(function() { +// var mainFileContent = fs.readFileSync(mainFile, 'utf8'); +// var routesFileContent = fs.readFileSync(routesFile, 'utf8'); +// expect(mainFileContent).to.contain('import { TestRoute } from \'./app/test-route/test-route.service.ts\''); +// if (!isMobileTest()){ +// expect(mainFileContent).to.contain('bootstrap(AppComponent, [ provideRouter(routes), TestRoute ])'); +// } else { +// expect(mainFileContent).to.contain('bootstrap(AppComponent, [ APP_SHELL_RUNTIME_PROVIDERS, provideRouter(routes), TestRoute ])'); +// } + +// expect(routesFileContent).to.contain('import { TestRoute } from \'./app/test-route/test-route.service.ts\''); +// expect(routesFileContent).to.contain('[\n { path: \'test-route\', component: TestRouteComponent: canActivate: [TestRoute] }\n ]') +// }) +// }) \ No newline at end of file diff --git a/addon/ng2/blueprints/routes/files/__path__/routes.ts b/addon/ng2/blueprints/routes/files/__path__/routes.ts new file mode 100644 index 000000000000..4905173f222f --- /dev/null +++ b/addon/ng2/blueprints/routes/files/__path__/routes.ts @@ -0,0 +1,2 @@ + +export default []; diff --git a/addon/ng2/blueprints/routes/index.js b/addon/ng2/blueprints/routes/index.js new file mode 100644 index 000000000000..96868fb19e03 --- /dev/null +++ b/addon/ng2/blueprints/routes/index.js @@ -0,0 +1,99 @@ +const Blueprint = require('ember-cli/lib/models/blueprint'); +const getFiles = Blueprint.prototype.files; +const path = require('path'); +const fs = require('fs'); +var chalk = require('chalk'); +const util = require('../../utilities/route-utils'); +const SilentError = require('silent-error'); + +module.exports = { + description: 'Generates a route and template', + + availableOptions: [ + { name: 'default', type: Boolean, default: false }, + { name: 'route', type: String }, + { name: 'parent', type: String, default: '' }, + { name: 'outlet', type: Boolean, default: false }, + { name: 'with-component', type: Boolean, default: false, aliases: ['wc'] } // isn't functional yet + ], + + beforeInstall: function(options){ + this._locals(options) + .then(names => { + var directory = this.newRoutePath[0] === path.sep ? + path.resolve(path.join(this.project.root, 'src', 'app', this.newRoutePath)): + path.resolve(process.env.PWD, this.newRoutePath); + // get route to be used in routes.ts + var route = directory.replace(path.join(this.project.root, 'src', 'app'), ''); + var parsedRoute = path.parse(route); + var routeName = parsedRoute.name.split('.')[0]; + // take care of the cases /**/home/home.component.ts vs /**/home + route = routeName === path.parse(parsedRoute.dir).name ? + path.parse(route).dir : `${parsedRoute.dir}/${routeName}`; + // setup options needed for adding path to routes.ts + this.pathOptions = { + isDefault: options.default, + route: options.route || route, + parent: options.parent, + outlet: options.outlet, + component: `${names.classifiedModuleName}Component`, + dasherizedName: names.dasherizedModuleName, + mainFile: path.join(this.project.root, 'src/main.ts'), + routesFile: path.join(this.project.root, 'src/routes.ts') + }; + }); + }, + + files: function() { + var fileList = getFiles.call(this); + if (this.project && fs.existsSync(path.join(this.project.root, 'src/routes.ts'))) { + return []; + } + return fileList; + }, + + fileMapTokens: function() { + return { + __path__: () => 'src' + }; + }, + + normalizeEntityName: function(entityName) { + this.newRoutePath = entityName + entityName = path.parse(entityName).name; + if (!entityName) { + throw new SilentError('Please provide new route\'s name'); + } + return entityName.split('.')[0]; + }, + + afterInstall: function() { + return Promise.resolve().then(() => { + // confirm componentFile and presence of export of the component in componentFile + var component = this.pathOptions.component; + var file = util.resolveComponentPath(this.project.root, process.env.PWD, this.newRoutePath); + if (!util.confirmComponentExport(file, component)) { + throw new SilentError(`Please add export for '${component}' to '${file}'`); + } + return Promise.resolve(); + }).then(() => { + // update routes in routes.ts + return util.applyChanges(util.addPathToRoutes(this.pathOptions.routesFile, this.pathOptions)); + }).then(() => { + // add import to routes.ts in main.ts + return util.applyChanges( + [util.insertImport(this.pathOptions.mainFile, 'routes', './routes', true)]); + }).then(() => { + // bootstrap routes + var routes = { 'provideRouter': ['@angular/router'] }; + var toBootstrap = 'provideRouter(routes)'; + return util.applyChanges(util.bootstrapItem(this.pathOptions.mainFile, routes, toBootstrap)); + }).catch(e => { + if (e.message.indexOf('Did not bootstrap') !== -1) { + this._writeStatusToUI(chalk.yellow, e.message, ''); + } else { + throw new SilentError(e.message); + } + }); + } +} diff --git a/addon/ng2/utilities/route-utils.ts b/addon/ng2/utilities/route-utils.ts index 34716bfefa97..023e1634061d 100644 --- a/addon/ng2/utilities/route-utils.ts +++ b/addon/ng2/utilities/route-utils.ts @@ -21,12 +21,13 @@ export function bootstrapItem(mainFile, imports: {[key: string]: [string, boolea // get ExpressionStatements from the top level syntaxList of the sourceFile let bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(node => { // get bootstrap expressions - return node.kind === ts.SyntaxKind.ExpressionStatement && - node.getChildAt(0).getChildAt(0).text.toLowerCase() === 'bootstrap'; + return node.kind === ts.SyntaxKind.ExpressionStatement && node.getChildAt(0) && + node.getChildAt(0).getChildAt(0) && node.getChildAt(0).getChildAt(0).text + && node.getChildAt(0).getChildAt(0).text.toLowerCase() === 'bootstrap'; }); if (bootstrapNodes.length !== 1) { throw new Error(`Did not bootstrap provideRouter in ${mainFile}` + - ' because of multiple or no bootstrap calls'); + ' because of multiple or no traditional bootstrap calls'); } let bootstrapNode = bootstrapNodes[0].getChildAt(0); let isBootstraped = findNodes(bootstrapNode, ts.SyntaxKind.SyntaxList) // get bootstrapped items @@ -520,3 +521,10 @@ function getValueForKey(objectLiteralNode: ts.TypeNode.ObjectLiteralExpression, function getRootNode(file: string) { return ts.createSourceFile(file, fs.readFileSync(file).toString(), ts.ScriptTarget.ES6, true); } + +function printAll(node, d = 0) { + let text = node.text ? `-----> ${node.text}` : ''; + console.log(new Array(d).join('####'), ts.SyntaxKind[node.kind], text); + d++; + node.getChildren().forEach(_ => printAll(_, d)); +} diff --git a/angular-cli-1.0.0-beta.9.tgz b/angular-cli-1.0.0-beta.9.tgz new file mode 100644 index 000000000000..4afa91154c5b Binary files /dev/null and b/angular-cli-1.0.0-beta.9.tgz differ diff --git a/tests/acceptance/route-utils.spec.ts b/tests/acceptance/route-utils.spec.ts index 6d1a7c48f8dd..49039429d217 100644 --- a/tests/acceptance/route-utils.spec.ts +++ b/tests/acceptance/route-utils.spec.ts @@ -193,7 +193,7 @@ describe('route utils', () => { .then(() => nru.bootstrapItem(mainFile, routes, toBootstrap)) .catch(e => expect(e.message).to.equal('Did not bootstrap provideRouter in' + - ' tmp/main.ts because of multiple or no bootstrap calls') + ' tmp/main.ts because of multiple or no traditional bootstrap calls') ); }); it('configures correctly if bootstrap or provide router is not at top level', () => { diff --git a/tests/e2e/e2e_workflow.spec.js b/tests/e2e/e2e_workflow.spec.js index d4522f6e4725..87d779c4e8f7 100644 --- a/tests/e2e/e2e_workflow.spec.js +++ b/tests/e2e/e2e_workflow.spec.js @@ -257,6 +257,22 @@ describe('Basic end-to-end Workflow', function () { }); }); + it('creates routes.ts, configures main.ts, and adds routes', function() { + return ng(['generate', 'component', 'test-route']).then(function() { + return ng(['generate', 'routes', `${path.sep}test-route`]).then(function () { + var routesFile = path.join(process.cwd(), 'src', 'routes.ts'); + var mainFile = path.join(process.cwd(), 'src', 'main.ts'); + expect(existsSync(routesFile)).to.be.truthy; + var mainFileContent = fs.readFileSync(mainFile, 'utf8'); + var routesFileContent = fs.readFileSync(routesFile, 'utf8'); + + expect(mainFileContent).to.contain('import routes from \'./routes\';'); + expect(routesFileContent).to.contain('import { TestRouteComponent } from \'./app/test-route/test-route.component\';'); + expect(routesFileContent).to.contain('[\n { path: \'test-route\', component: TestRouteComponent }\n]'); + }); + }); + }); + xit('Perform `ng test` after adding a route', function () { this.timeout(420000);