From a5735569633c9f7b947ed33d11540c39d8b0e077 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 30 Nov 2016 15:13:29 +0100 Subject: [PATCH] initial commit --- .editorconfig | 15 ++ .gitignore | 20 +++ .vscode/launch.json | 46 ++++++ .vscode/settings.json | 3 + README.md | 33 ++++ app.json | 12 ++ empty.js | 7 + nodemon.json | 7 + package.json | 98 ++++++++++++ src/+app/+about/about-routing.module.ts | 13 ++ src/+app/+about/about.component.ts | 14 ++ src/+app/+about/about.module.ts | 16 ++ src/+app/+home/home-routing.module.ts | 13 ++ src/+app/+home/home.component.css | 8 + src/+app/+home/home.component.html | 6 + src/+app/+home/home.component.ts | 27 ++++ src/+app/+home/home.module.ts | 16 ++ src/+app/+lazy/lazy-routing.module.ts | 13 ++ src/+app/+lazy/lazy.component.ts | 14 ++ src/+app/+lazy/lazy.module.ts | 16 ++ src/+app/+todo/todo-routing.module.ts | 13 ++ src/+app/+todo/todo.component.ts | 43 ++++++ src/+app/+todo/todo.module.ts | 16 ++ src/+app/app-routing.module.ts | 17 +++ src/+app/app.component.ts | 66 ++++++++ src/+app/app.module.ts | 27 ++++ src/+app/shared/api.service.ts | 29 ++++ src/+app/shared/cache.service.ts | 88 +++++++++++ src/+app/shared/model/model.service.ts | 55 +++++++ src/+app/shared/shared.module.ts | 52 +++++++ src/__workaround.browser.ts | 21 +++ src/__workaround.node.ts | 44 ++++++ src/angular2-meta.ts | 194 ++++++++++++++++++++++++ src/assets/logo.png | Bin 0 -> 22394 bytes src/backend/api.ts | 106 +++++++++++++ src/backend/cache.ts | 17 +++ src/backend/db.ts | 7 + src/browser.module.ts | 97 ++++++++++++ src/client.aot.ts | 36 +++++ src/client.ts | 34 +++++ src/index.html | 23 +++ src/node.module.ts | 73 +++++++++ src/server.aot.ts | 147 ++++++++++++++++++ src/server.routes.ts | 17 +++ src/server.ts | 103 +++++++++++++ src/typings.d.ts | 73 +++++++++ tsconfig.aot.json | 28 ++++ tsconfig.json | 27 ++++ webpack.config.ts | 133 ++++++++++++++++ webpack.prod.config.ts | 178 ++++++++++++++++++++++ 50 files changed, 2161 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 README.md create mode 100644 app.json create mode 100644 empty.js create mode 100644 nodemon.json create mode 100644 package.json create mode 100644 src/+app/+about/about-routing.module.ts create mode 100644 src/+app/+about/about.component.ts create mode 100644 src/+app/+about/about.module.ts create mode 100644 src/+app/+home/home-routing.module.ts create mode 100644 src/+app/+home/home.component.css create mode 100644 src/+app/+home/home.component.html create mode 100644 src/+app/+home/home.component.ts create mode 100644 src/+app/+home/home.module.ts create mode 100644 src/+app/+lazy/lazy-routing.module.ts create mode 100644 src/+app/+lazy/lazy.component.ts create mode 100644 src/+app/+lazy/lazy.module.ts create mode 100644 src/+app/+todo/todo-routing.module.ts create mode 100644 src/+app/+todo/todo.component.ts create mode 100644 src/+app/+todo/todo.module.ts create mode 100644 src/+app/app-routing.module.ts create mode 100644 src/+app/app.component.ts create mode 100755 src/+app/app.module.ts create mode 100644 src/+app/shared/api.service.ts create mode 100644 src/+app/shared/cache.service.ts create mode 100644 src/+app/shared/model/model.service.ts create mode 100644 src/+app/shared/shared.module.ts create mode 100644 src/__workaround.browser.ts create mode 100644 src/__workaround.node.ts create mode 100644 src/angular2-meta.ts create mode 100644 src/assets/logo.png create mode 100644 src/backend/api.ts create mode 100644 src/backend/cache.ts create mode 100644 src/backend/db.ts create mode 100755 src/browser.module.ts create mode 100644 src/client.aot.ts create mode 100644 src/client.ts create mode 100644 src/index.html create mode 100755 src/node.module.ts create mode 100644 src/server.aot.ts create mode 100644 src/server.routes.ts create mode 100644 src/server.ts create mode 100644 src/typings.d.ts create mode 100644 tsconfig.aot.json create mode 100644 tsconfig.json create mode 100644 webpack.config.ts create mode 100644 webpack.prod.config.ts diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..f1cc3ad329c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000000..91729af70e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/__build__ +/__server_build__ +/node_modules +/typings +/tsd_typings/ +npm-debug.log + +/dist/ + +.idea +*.ngfactory.ts +*.css.shim.ts + +.DS_Store + +webpack.records.json + +/npm-debug.log.* + +morgan.log diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..f2449acd54f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,46 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/dist/server/index.js", + "stopOnEntry": false, + "args": [], + "cwd": "${workspaceRoot}", + "preLaunchTask": null, + "runtimeExecutable": null, + "runtimeArgs": [ + "--nolazy" + ], + "env": { + "NODE_ENV": "development" + }, + "externalConsole": false, + "sourceMaps": false, + "outDir": null + }, + { + "name": "Attach", + "type": "node", + "request": "attach", + "port": 5858, + "address": "localhost", + "restart": false, + "sourceMaps": false, + "outDir": null, + "localRoot": "${workspaceRoot}", + "remoteRoot": null + }, + { + "name": "Attach to Process", + "type": "node", + "request": "attach", + "processId": "${command.PickProcess}", + "port": 5858, + "sourceMaps": false, + "outDir": null + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..ccdda087ebd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.check.workspaceVersion": false +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000000..50352981aba --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ + +# dspace-angular +> A UI for DSpace based on Angular 2 Universal. + +Currently this contains the [Angular 2 Universal Starter](https://github.com/angular/universal-starter) codebase with a few tweaks. + +## Links +- [The working group page on the DuraSpace Wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Working+Group) +- [The project board (waffle.io)](https://waffle.io/DSpace/dspace-angular) +- [The prototype](https://github.com/DSpace-Labs/angular2-ui-prototype) + +## Requirements + - [Node.js](https://nodejs.org/) + +## Installation +- `npm install` + +## Serve +- `npm start` to build your client app and start a web server +- `npm run build` to prepare a distributable bundle + +## Watch files +- `npm run watch` to build your client app and start a web server + +## Development +- run `npm start` and `npm run watch` in two separate terminals to build your client app, start a web server, and allow file changes to update in realtime + +## AoT and Prod +- `npm run build:prod:ngc` to compile the ngfactory files and build prod + + + + diff --git a/app.json b/app.json new file mode 100644 index 00000000000..1762bb6b8bd --- /dev/null +++ b/app.json @@ -0,0 +1,12 @@ +{ + "name": "Angular 2 Universal Starter", + "description": "Angular 2 Universal starter kit by @AngularClass", + "repository": "https://github.com/angular/universal-starter", + "logo": "https://cloud.githubusercontent.com/assets/1016365/10639063/138338bc-7806-11e5-8057-d34c75f3cafc.png", + "env": { + "NPM_CONFIG_PRODUCTION": { + "description": "Install `webpack` and other development modules when deploying to allow full builds.", + "value": "false" + } + } +} diff --git a/empty.js b/empty.js new file mode 100644 index 00000000000..b8ab065d5ac --- /dev/null +++ b/empty.js @@ -0,0 +1,7 @@ +module.exports = { + NgProbeToken: {}, + _createConditionalRootRenderer: function(rootRenderer, extraTokens, coreTokens) { + return rootRenderer; + }, + __platform_browser_private__: {} +}; diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 00000000000..97a836fbb5c --- /dev/null +++ b/nodemon.json @@ -0,0 +1,7 @@ +{ + "watch": [ + "dist", + "src/index.html" + ], + "ext" : "js ts json html" +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000..aa348c0aec0 --- /dev/null +++ b/package.json @@ -0,0 +1,98 @@ +{ + "name": "dspace-angular", + "version": "0.0.0", + "description": "Angular 2 Universal UI for DSpace", + "repository": { + "type": "git", + "url": "https://github.com/dspace/dspace-angular.git" + }, + "scripts": { + "watch": "webpack --watch", + "watch:dev": "npm run server & npm run watch", + "clean:dist": "rimraf dist", + "clean:ngc": "rimraf **/*.ngfactory.ts **/*.css.shim.ts", + "prebuild": "npm run clean:dist", + "build": "webpack --progress", + "build:prod:ngc": "npm run clean:ngc && npm run ngc && npm run clean:dist && npm run build:prod", + "build:prod:ngc:json": "npm run clean:ngc && npm run ngc && npm run clean:dist && npm run build:prod:json", + "build:prod": "webpack --config webpack.prod.config.ts", + "build:prod:json": "webpack --config webpack.prod.config.ts --json | webpack-bundle-size-analyzer", + "ngc": "ngc -p tsconfig.aot.json", + "prestart": "npm run build", + "server": "nodemon dist/server/index.js", + "debug:server": "node-nightly --inspect --debug-brk dist/server/index.js", + "start": "npm run server", + "debug:start": "npm run build && npm run debug:server", + "predebug": "npm run build", + "debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js", + "debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --config webpack.prod.config.ts", + "debug": "node --debug-brk dist/server/index.js" + }, + "dependencies": { + "@angular/common": "~2.1.2", + "@angular/compiler": "~2.1.2", + "@angular/compiler-cli": "~2.1.2", + "@angular/core": "~2.1.2", + "@angular/forms": "~2.1.2", + "@angular/http": "~2.1.2", + "@angular/platform-browser": "~2.1.2", + "@angular/platform-browser-dynamic": "~2.1.2", + "@angular/platform-server": "~2.1.2", + "@angular/router": "~3.1.2", + "@angular/upgrade": "~2.1.2", + "@angularclass/bootloader": "~1.0.1", + "@angularclass/idle-preload": "~1.0.4", + "angular2-express-engine": "~2.1.0-rc.1", + "angular2-platform-node": "~2.1.0-rc.1", + "angular2-universal": "~2.1.0-rc.1", + "angular2-universal-polyfills": "~2.1.0-rc.1", + "body-parser": "^1.15.2", + "compression": "^1.6.2", + "express": "^4.14.0", + "js.clone": "0.0.3", + "methods": "~1.1.2", + "morgan": "^1.7.0", + "preboot": "~4.5.2", + "rxjs": "5.0.0-beta.12", + "webfontloader": "^1.6.26", + "zone.js": "~0.6.26" + }, + "devDependencies": { + "@types/morgan": "^1.7.32", + "@types/body-parser": "0.0.29", + "@types/compression": "0.0.29", + "@types/cookie-parser": "^1.3.29", + "@types/express": "^4.0.32", + "@types/express-serve-static-core": "^4.0.33", + "@types/hammerjs": "^2.0.32", + "@types/memory-cache": "0.0.29", + "@types/mime": "0.0.28", + "@types/node": "^6.0.38", + "@types/serve-static": "^1.7.27", + "@types/webfontloader": "^1.6.27", + "@ngtools/webpack": "~1.1.7", + "accepts": "^1.3.3", + "angular2-template-loader": "^0.4.0", + "awesome-typescript-loader": "^2.2.4", + "cookie-parser": "^1.4.3", + "express-interceptor": "^1.2.0", + "iltorb": "^1.0.13", + "imports-loader": "^0.6.5", + "json-loader": "^0.5.4", + "memory-cache": "^0.1.6", + "nodemon": "^1.10.0", + "raw-loader": "^0.5.1", + "reflect-metadata": "0.1.8", + "rimraf": "^2.5.4", + "string-replace-loader": "^1.0.5", + "ts-helpers": "^1.1.2", + "ts-node": "^1.3.0", + "typescript": "2.0.2", + "v8-lazy-parse-webpack-plugin": "^0.3.0", + "webpack": "2.1.0-beta.27", + "webpack-bundle-analyzer": "1.4.1", + "webpack-dev-middleware": "^1.8.4", + "webpack-dev-server": "2.1.0-beta.11", + "webpack-merge": "~0.16.0" + } +} diff --git a/src/+app/+about/about-routing.module.ts b/src/+app/+about/about-routing.module.ts new file mode 100644 index 00000000000..2a2f493b2f3 --- /dev/null +++ b/src/+app/+about/about-routing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AboutComponent } from './about.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: 'about', component: AboutComponent } + ]) + ] +}) +export class AboutRoutingModule { } diff --git a/src/+app/+about/about.component.ts b/src/+app/+about/about.component.ts new file mode 100644 index 00000000000..08f97f936f2 --- /dev/null +++ b/src/+app/+about/about.component.ts @@ -0,0 +1,14 @@ +import { Component, Inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; + +@Component({ + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated, + selector: 'about', + template: 'About component' +}) +export class AboutComponent { + constructor(@Inject('req') req: any) { + // console.log('req', req) + + } +} diff --git a/src/+app/+about/about.module.ts b/src/+app/+about/about.module.ts new file mode 100644 index 00000000000..32b2461a3c0 --- /dev/null +++ b/src/+app/+about/about.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { AboutComponent } from './about.component'; +import { AboutRoutingModule } from './about-routing.module'; + +@NgModule({ + imports: [ + SharedModule, + AboutRoutingModule + ], + declarations: [ + AboutComponent + ] +}) +export class AboutModule { } diff --git a/src/+app/+home/home-routing.module.ts b/src/+app/+home/home-routing.module.ts new file mode 100644 index 00000000000..62a0799f2fe --- /dev/null +++ b/src/+app/+home/home-routing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { HomeComponent } from './home.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: 'home', component: HomeComponent } + ]) + ] +}) +export class HomeRoutingModule { } diff --git a/src/+app/+home/home.component.css b/src/+app/+home/home.component.css new file mode 100644 index 00000000000..17b877f70b4 --- /dev/null +++ b/src/+app/+home/home.component.css @@ -0,0 +1,8 @@ +blockquote { + border-left:5px #158126 solid; + background:#fff; + padding:20px 20px 20px 40px; +} +blockquote::before { + left: 1em; +} diff --git a/src/+app/+home/home.component.html b/src/+app/+home/home.component.html new file mode 100644 index 00000000000..75663111f7b --- /dev/null +++ b/src/+app/+home/home.component.html @@ -0,0 +1,6 @@ +
+ Home component + Async data call return value: +
{{ data | json }}
+
{{ data.data }}
+
diff --git a/src/+app/+home/home.component.ts b/src/+app/+home/home.component.ts new file mode 100644 index 00000000000..1d18f3e0618 --- /dev/null +++ b/src/+app/+home/home.component.ts @@ -0,0 +1,27 @@ +import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; + +import { ModelService } from '../shared/model/model.service'; + +@Component({ + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated, + selector: 'home', + styleUrls: [ './home.component.css' ], + templateUrl: './home.component.html' +}) +export class HomeComponent { + data: any = {}; + constructor(public model: ModelService) { + + // we need the data synchronously for the client to set the server response + // we create another method so we have more control for testing + this.universalInit(); + } + + universalInit() { + this.model.get('/data.json').subscribe(data => { + this.data = data; + }); + } + +} diff --git a/src/+app/+home/home.module.ts b/src/+app/+home/home.module.ts new file mode 100644 index 00000000000..3d8028cbb78 --- /dev/null +++ b/src/+app/+home/home.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { HomeComponent } from './home.component'; +import { HomeRoutingModule } from './home-routing.module'; + +@NgModule({ + imports: [ + SharedModule, + HomeRoutingModule + ], + declarations: [ + HomeComponent + ] +}) +export class HomeModule { } diff --git a/src/+app/+lazy/lazy-routing.module.ts b/src/+app/+lazy/lazy-routing.module.ts new file mode 100644 index 00000000000..8391fc4f6a6 --- /dev/null +++ b/src/+app/+lazy/lazy-routing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { LazyComponent } from './lazy.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: '', component: LazyComponent } + ]) + ] +}) +export class LazyRoutingModule { } diff --git a/src/+app/+lazy/lazy.component.ts b/src/+app/+lazy/lazy.component.ts new file mode 100644 index 00000000000..98b3a39693b --- /dev/null +++ b/src/+app/+lazy/lazy.component.ts @@ -0,0 +1,14 @@ +import { Component, Inject, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; + +@Component({ + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated, + selector: 'lazy', + template: ` +

+ Lazy component +

+ ` +}) +export class LazyComponent { +} diff --git a/src/+app/+lazy/lazy.module.ts b/src/+app/+lazy/lazy.module.ts new file mode 100644 index 00000000000..0fb8ee061c5 --- /dev/null +++ b/src/+app/+lazy/lazy.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { LazyComponent } from './lazy.component'; +import { LazyRoutingModule } from './lazy-routing.module'; + +@NgModule({ + imports: [ + SharedModule, + LazyRoutingModule + ], + declarations: [ + LazyComponent + ] +}) +export class LazyModule { } diff --git a/src/+app/+todo/todo-routing.module.ts b/src/+app/+todo/todo-routing.module.ts new file mode 100644 index 00000000000..5f84c3eafa7 --- /dev/null +++ b/src/+app/+todo/todo-routing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { TodoComponent } from './todo.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: 'todo', component: TodoComponent } + ]) + ] +}) +export class TodoRoutingModule { } diff --git a/src/+app/+todo/todo.component.ts b/src/+app/+todo/todo.component.ts new file mode 100644 index 00000000000..11d7dd66b9b --- /dev/null +++ b/src/+app/+todo/todo.component.ts @@ -0,0 +1,43 @@ +import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; + +import { ModelService } from '../shared/model/model.service'; + +@Component({ + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated, + selector: 'todo', + styles: [` + `], + template: ` +
+ Todo component +
+ + +
+ +
+ ` +}) +export class TodoComponent { + newTodo = ''; + todos = []; + constructor(public model: ModelService) { + // we need the data synchronously for the client to set the server response + // we create another method so we have more control for testing + this.universalInit(); + } + + addTodo(newTodo) { + this.todos.push(newTodo); + this.newTodo = ''; + } + + universalInit() { + } + +} diff --git a/src/+app/+todo/todo.module.ts b/src/+app/+todo/todo.module.ts new file mode 100644 index 00000000000..94704296416 --- /dev/null +++ b/src/+app/+todo/todo.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { TodoComponent } from './todo.component'; +import { TodoRoutingModule } from './todo-routing.module'; + +@NgModule({ + imports: [ + SharedModule, + TodoRoutingModule + ], + declarations: [ + TodoComponent + ] +}) +export class TodoModule { } diff --git a/src/+app/app-routing.module.ts b/src/+app/app-routing.module.ts new file mode 100644 index 00000000000..be947e59f2d --- /dev/null +++ b/src/+app/app-routing.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +export function getLazyModule() { + return System.import('./+lazy/lazy.module' + (process.env.AOT ? '.ngfactory' : '')) + .then(mod => mod[(process.env.AOT ? 'LazyModuleNgFactory' : 'LazyModule')]); +} + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { path: 'lazy', loadChildren: getLazyModule } + ]) + ], +}) +export class AppRoutingModule { } diff --git a/src/+app/app.component.ts b/src/+app/app.component.ts new file mode 100644 index 00000000000..97617d346a8 --- /dev/null +++ b/src/+app/app.component.ts @@ -0,0 +1,66 @@ +import { Component, Directive, ElementRef, Renderer, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core'; + +// +///////////////////////// +// ** Example Directive +// Notice we don't touch the Element directly + +@Directive({ + selector: '[xLarge]' +}) +export class XLargeDirective { + constructor(element: ElementRef, renderer: Renderer) { + // ** IMPORTANT ** + // we must interact with the dom through -Renderer- + // for webworker/server to see the changes + renderer.setElementStyle(element.nativeElement, 'fontSize', 'x-large'); + // ^^ + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.Default, + encapsulation: ViewEncapsulation.Emulated, + selector: 'app', + styles: [` + * { padding:0; margin:0; font-family: 'Droid Sans', sans-serif; } + #universal { text-align:center; font-weight:bold; padding:15px 0; } + nav { background:#158126; min-height:40px; border-bottom:5px #046923 solid; } + nav a { font-weight:bold; text-decoration:none; color:#fff; padding:20px; display:inline-block; } + nav a:hover { background:#00AF36; } + .hero-universal { min-height:500px; display:block; padding:20px; background: url('/assets/logo.png') no-repeat center center; } + .inner-hero { background: rgba(255, 255, 255, 0.75); border:5px #ccc solid; padding:25px; } + .router-link-active { background-color: #00AF36; } + main { padding:20px 0; } + pre { font-size:12px; } + `], + template: ` +

Angular2 Universal

+ +
+
+
+ Universal JavaScript {{ title }}! +
+ + Two-way binding: + +
+
+ + Router-outlet: +
+ +
+
+
+ ` +}) +export class AppComponent { + title = 'ftw'; +} diff --git a/src/+app/app.module.ts b/src/+app/app.module.ts new file mode 100755 index 00000000000..4aa6b5fac24 --- /dev/null +++ b/src/+app/app.module.ts @@ -0,0 +1,27 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { HomeModule } from './+home/home.module'; +import { AboutModule } from './+about/about.module'; +import { TodoModule } from './+todo/todo.module'; + +import { SharedModule } from './shared/shared.module'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent, XLargeDirective } from './app.component'; + + +@NgModule({ + declarations: [ AppComponent, XLargeDirective ], + imports: [ + SharedModule, + HomeModule, + AboutModule, + TodoModule, + AppRoutingModule + ] +}) +export class AppModule { +} + +export { AppComponent } from './app.component'; diff --git a/src/+app/shared/api.service.ts b/src/+app/shared/api.service.ts new file mode 100644 index 00000000000..5c5aa2611a4 --- /dev/null +++ b/src/+app/shared/api.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/throw'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; + +import { CacheService } from './cache.service'; + + +@Injectable() +export class ApiService { + constructor(public _http: Http) { + + } + + /** + * whatever domain/feature method name + */ + get(url: string, options?: any) { + return this._http.get(url, options) + .map(res => res.json()) + .catch(err => { + console.log('Error: ', err); + return Observable.throw(err); + }); + } + +} diff --git a/src/+app/shared/cache.service.ts b/src/+app/shared/cache.service.ts new file mode 100644 index 00000000000..87780632e50 --- /dev/null +++ b/src/+app/shared/cache.service.ts @@ -0,0 +1,88 @@ +import { Inject, Injectable, isDevMode } from '@angular/core'; + +@Injectable() +export class CacheService { + static KEY = 'CacheService'; + + constructor(@Inject('LRU') public _cache: Map) { + + } + + /** + * check if there is a value in our store + */ + has(key: string | number): boolean { + let _key = this.normalizeKey(key); + return this._cache.has(_key); + } + + /** + * store our state + */ + set(key: string | number, value: any): void { + let _key = this.normalizeKey(key); + this._cache.set(_key, value); + } + + /** + * get our cached value + */ + get(key: string | number): any { + let _key = this.normalizeKey(key); + return this._cache.get(_key); + } + + /** + * release memory refs + */ + clear(): void { + this._cache.clear(); + } + + /** + * convert to json for the client + */ + dehydrate(): any { + let json = {}; + this._cache.forEach((value: any, key: string) => json[key] = value); + return json; + } + + /** + * convert server json into out initial state + */ + rehydrate(json: any): void { + Object.keys(json).forEach((key: string) => { + let _key = this.normalizeKey(key); + let value = json[_key]; + this._cache.set(_key, value); + }); + } + + /** + * allow JSON.stringify to work + */ + toJSON(): any { + return this.dehydrate(); + } + + /** + * convert numbers into strings + */ + normalizeKey(key: string | number): string { + if (isDevMode() && this._isInvalidValue(key)) { + throw new Error('Please provide a valid key to save in the CacheService'); + } + + return key + ''; + } + + _isInvalidValue(key): boolean { + return key === null || + key === undefined || + key === 0 || + key === '' || + typeof key === 'boolean' || + Number.isNaN(key); + } +} diff --git a/src/+app/shared/model/model.service.ts b/src/+app/shared/model/model.service.ts new file mode 100644 index 00000000000..7d44a2b223a --- /dev/null +++ b/src/+app/shared/model/model.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/share'; + +import { CacheService } from '../cache.service'; +import { ApiService } from '../api.service'; + +export function hashCodeString(str: string): string { + let hash = 0; + if (str.length === 0) { + return hash + ''; + } + for (let i = 0; i < str.length; i++) { + let char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash + ''; +} + +// domain/feature service +@Injectable() +export class ModelService { + // This is only one example of one Model depending on your domain + constructor(public _api: ApiService, public _cache: CacheService) { + + } + + /** + * whatever domain/feature method name + */ + get(url) { + // you want to return the cache if there is a response in it. + // This would cache the first response so if your API isn't idempotent + // you probably want to remove the item from the cache after you use it. LRU of 10 + // you can use also hashCodeString here + let key = url; + + if (this._cache.has(key)) { + return Observable.of(this._cache.get(key)); + } + // you probably shouldn't .share() and you should write the correct logic + return this._api.get(url) + .do(json => { + this._cache.set(key, json); + }) + .share(); + } + // don't cache here since we're creating + create() { + // TODO + } +} diff --git a/src/+app/shared/shared.module.ts b/src/+app/shared/shared.module.ts new file mode 100644 index 00000000000..a99fcdea092 --- /dev/null +++ b/src/+app/shared/shared.module.ts @@ -0,0 +1,52 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ApiService } from './api.service'; +import { ModelService } from './model/model.service'; + +const MODULES = [ + // Do NOT include UniversalModule, HttpModule, or JsonpModule here + CommonModule, + RouterModule, + FormsModule, + ReactiveFormsModule +]; + +const PIPES = [ + // put pipes here +]; + +const COMPONENTS = [ + // put shared components here +]; + +const PROVIDERS = [ + ModelService, + ApiService +] + +@NgModule({ + imports: [ + ...MODULES + ], + declarations: [ + ...PIPES, + ...COMPONENTS + ], + exports: [ + ...MODULES, + ...PIPES, + ...COMPONENTS + ] +}) +export class SharedModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: SharedModule, + providers: [ + ...PROVIDERS + ] + }; + } +} diff --git a/src/__workaround.browser.ts b/src/__workaround.browser.ts new file mode 100644 index 00000000000..026f505fa37 --- /dev/null +++ b/src/__workaround.browser.ts @@ -0,0 +1,21 @@ + +/* + * THIS IS TEMPORARY TO PATCH 2.1.1+ Core bugs + */ + +/* tslint:disable */ +let __compiler__ = require('@angular/compiler'); +import { __platform_browser_private__ } from '@angular/platform-browser'; +import { __core_private__ } from '@angular/core'; +if (!__core_private__['ViewUtils']) { + __core_private__['ViewUtils'] = __core_private__['view_utils']; +} + + + +if (__compiler__ && __compiler__.SelectorMatcher && __compiler__.CssSelector) { + (__compiler__).__compiler_private__ = { + SelectorMatcher: __compiler__.SelectorMatcher, + CssSelector: __compiler__.CssSelector + } +} diff --git a/src/__workaround.node.ts b/src/__workaround.node.ts new file mode 100644 index 00000000000..ed39c8d3195 --- /dev/null +++ b/src/__workaround.node.ts @@ -0,0 +1,44 @@ + +/* + * THIS IS TEMPORARY TO PATCH 2.1.1+ Core bugs + */ + +/* tslint:disable */ +let __compiler__ = require('@angular/compiler'); +import { __platform_browser_private__ } from '@angular/platform-browser'; +import { __core_private__ } from '@angular/core'; +let patch = false; +if (!__core_private__['ViewUtils']) { + patch = true; + __core_private__['ViewUtils'] = __core_private__['view_utils']; +} + + + +if (__compiler__ && __compiler__.SelectorMatcher && __compiler__.CssSelector) { + patch = true; + (__compiler__).__compiler_private__ = { + SelectorMatcher: __compiler__.SelectorMatcher, + CssSelector: __compiler__.CssSelector + } +} + +if (patch) { + var __universal__ = require('angular2-platform-node/__private_imports__'); + __universal__.ViewUtils = __core_private__['view_utils']; + __universal__.CssSelector = __universal__.CssSelector || __compiler__.CssSelector; + __universal__.SelectorMatcher = __universal__.SelectorMatcher || __compiler__.SelectorMatcher; +} + +// Fix Material Support +function universalMaterialSupports(eventName: string): boolean { return Boolean(this.isCustomEvent(eventName)); } +__platform_browser_private__.HammerGesturesPlugin.prototype.supports = universalMaterialSupports; +// End Fix Material Support + +// Fix Universal Style +import { NodeDomRootRenderer, NodeDomRenderer } from 'angular2-universal/node'; +function renderComponentFix(componentProto: any) { + return new NodeDomRenderer(this, componentProto, this._animationDriver); +} +NodeDomRootRenderer.prototype.renderComponent = renderComponentFix; +// End Fix Universal Style diff --git a/src/angular2-meta.ts b/src/angular2-meta.ts new file mode 100644 index 00000000000..f961d9e4fec --- /dev/null +++ b/src/angular2-meta.ts @@ -0,0 +1,194 @@ +/** + * @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 {Injectable} from '@angular/core'; +// es6-modules are used here +import {DomAdapter, getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +/** + * Represent meta element. + * + * ### Example + * + * ```ts + * { name: 'application-name', content: 'Name of my application' }, + * { name: 'description', content: 'A description of the page', id: 'desc' } + * // ... + * // Twitter + * { name: 'twitter:title', content: 'Content Title' } + * // ... + * // Google+ + * { itemprop: 'name', content: 'Content Title' }, + * { itemprop: 'description', content: 'Content Title' } + * // ... + * // Facebook / Open Graph + * { property: 'fb:app_id', content: '123456789' }, + * { property: 'og:title', content: 'Content Title' } + * ``` + * + * @experimental + */ +export interface MetaDefinition { + charset?: string; + content?: string; + httpEquiv?: string; + id?: string; + itemprop?: string; + name?: string; + property?: string; + scheme?: string; + url?: string; + [prop: string]: string; +} + +/** + * A service that can be used to get and add meta tags. + * + * @experimental + */ +@Injectable() +export class Meta { + private _dom: DomAdapter = getDOM(); + + /** + * Adds a new meta tag to the dom. + * + * ### Example + * + * ```ts + * const name: MetaDefinition = {name: 'application-name', content: 'Name of my application'}; + * const desc: MetaDefinition = {name: 'description', content: 'A description of the page'}; + * const tags: HTMLMetaElement[] = this.meta.addTags([name, desc]); + * ``` + * + * @param tags + * @returns {HTMLMetaElement[]} + */ + addTags(...tags: Array): HTMLMetaElement[] { + const presentTags = this._flattenArray(tags); + if (presentTags.length === 0) return []; + return presentTags.map((tag: MetaDefinition) => this._addInternal(tag)); + } + + /** + * Gets the meta tag by the given selector. Returns element or null + * if there's no such meta element. + * + * ### Example + * + * ```ts + * const meta: HTMLMetaElement = this.meta.getTag('name=description'); + * const twitterMeta: HTMLMetaElement = this.meta.getTag('name="twitter:title"'); + * const fbMeta: HTMLMetaElement = this.meta.getTag('property="fb:app_id"'); + * ``` + * + * @param selector + * @returns {HTMLMetaElement} + */ + getTag(selector: string): HTMLMetaElement { + if (!selector) return null; + return this._dom.query(`meta[${selector}]`); + } + + /** + * Updates the meta tag with the given selector. + * + * * ### Example + * + * ```ts + * const meta: HTMLMetaElement = this.meta.updateTag('name=description', {name: 'description', + * content: 'New description'}); + * console.log(meta.content); // 'New description' + * ``` + * + * @param selector + * @param tag updated tag definition + * @returns {HTMLMetaElement} + */ + updateTag(selector: string, tag: MetaDefinition): HTMLMetaElement { + const meta: HTMLMetaElement = this.getTag(selector); + if (!meta) { + // create element if it doesn't exist + return this._addInternal(tag); + } + return this._prepareMetaElement(tag, meta); + } + + /** + * Removes meta tag with the given selector from the dom. + * + * ### Example + * + * ```ts + * this.meta.removeTagBySelector('name=description'); + * ``` + * + * @param selector + */ + removeTagBySelector(selector: string): void { + const meta: HTMLMetaElement = this.getTag(selector); + this.removeTagElement(meta); + } + + /** + * Removes given meta element from the dom. + * + * ### Example + * ```ts + * const elem: HTMLMetaElement = this.meta.getTag('name=description'); + * this.meta.removeTagElement(elem); + * ``` + * + * @param meta meta element + */ + removeTagElement(meta: HTMLMetaElement): void { + if (meta) { + this._removeMetaElement(meta); + } + } + + private _addInternal(tag: MetaDefinition): HTMLMetaElement { + const meta: HTMLMetaElement = this._createMetaElement(); + this._prepareMetaElement(tag, meta); + this._appendMetaElement(meta); + return meta; + } + + private _createMetaElement(): HTMLMetaElement { + return this._dom.createElement('meta') as HTMLMetaElement; + } + + private _prepareMetaElement(tag: MetaDefinition, el: HTMLMetaElement): HTMLMetaElement { + Object.keys(tag).forEach((prop: string) => this._dom.setAttribute(el, prop, tag[prop])); + return el; + } + + private _appendMetaElement(meta: HTMLMetaElement): void { + const head = this._dom.getElementsByTagName(this._dom.defaultDoc(), 'head')[0]; + this._dom.appendChild(head, meta); + } + + private _removeMetaElement(meta: HTMLMetaElement): void { + const head = this._dom.parentElement(meta); + this._dom.removeChild(head, meta); + } + + private _flattenArray(input: any[], out: any[] = []): any[] { + if (input) { + for (let i = 0; i < input.length; i++) { + const item: any = input[i]; + if (Array.isArray(item)) { + this._flattenArray(item, out); + } else if (item) { + out.push(item); + } + } + } + return out; + } +} diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..afea237f93a9c9370901fe74e1ef5e1368381a48 GIT binary patch literal 22394 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ǂ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 { + fakeDemoRedisCache.set(key, data); + return data; + }) + .then(data => res.json(data)); +} + + +// todo API + +var COUNT = 4; +var TODOS = [ + { id: 0, value: 'finish example', created_at: new Date(), completed: false }, + { id: 1, value: 'add tests', created_at: new Date(), completed: false }, + { id: 2, value: 'include development environment', created_at: new Date(), completed: false }, + { id: 3, value: 'include production environment', created_at: new Date(), completed: false } +]; + +export function createTodoApi() { + + var router = Router() + + router.route('/todos') + .get(function(req, res) { + console.log('GET'); + // 70ms latency + setTimeout(function() { + res.json(TODOS); + }, 0); + + }) + .post(function(req, res) { + console.log('POST', util.inspect(req.body, {colors: true})); + var todo = req.body; + if (todo) { + TODOS.push({ + value: todo.value, + created_at: new Date(), + completed: todo.completed, + id: COUNT++ + }); + return res.json(todo); + } + + return res.end(); + }); + + router.param('todo_id', function(req, res, next, todo_id) { + // ensure correct prop type + var id = Number(req.params.todo_id); + try { + var todo = TODOS[id]; + req.todo_id = id; + req.todo = TODOS[id]; + next(); + } catch (e) { + next(new Error('failed to load todo')); + } + }); + + router.route('/todos/:todo_id') + .get(function(req, res) { + console.log('GET', util.inspect(req.todo, {colors: true})); + + res.json(req.todo); + }) + .put(function(req, res) { + console.log('PUT', util.inspect(req.body, {colors: true})); + + var index = TODOS.indexOf(req.todo); + var todo = TODOS[index] = req.body; + + res.json(todo); + }) + .delete(function(req, res) { + console.log('DELETE', req.todo_id); + + var index = TODOS.indexOf(req.todo); + TODOS.splice(index, 1); + + res.json(req.todo); + }); + + return router; +}; diff --git a/src/backend/cache.ts b/src/backend/cache.ts new file mode 100644 index 00000000000..490a35612d7 --- /dev/null +++ b/src/backend/cache.ts @@ -0,0 +1,17 @@ + + +var _fakeLRUcount = 0; +export const fakeDemoRedisCache = { + _cache: {}, + get: (key) => { + let cache = fakeDemoRedisCache._cache[key]; + _fakeLRUcount++; + if (_fakeLRUcount >= 10) { + fakeDemoRedisCache.clear(); + _fakeLRUcount = 0; + } + return cache; + }, + set: (key, data) => fakeDemoRedisCache._cache[key] = data, + clear: () => fakeDemoRedisCache._cache = {} +}; diff --git a/src/backend/db.ts b/src/backend/db.ts new file mode 100644 index 00000000000..3ba8ea3d445 --- /dev/null +++ b/src/backend/db.ts @@ -0,0 +1,7 @@ +// Our API for demos only +export const fakeDataBase = { + get() { + let res = { data: 'This fake data came from the db on the server.' }; + return Promise.resolve(res); + } +}; diff --git a/src/browser.module.ts b/src/browser.module.ts new file mode 100755 index 00000000000..a8ec09c73af --- /dev/null +++ b/src/browser.module.ts @@ -0,0 +1,97 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { UniversalModule, isBrowser, isNode, AUTO_PREBOOT } from 'angular2-universal/browser'; // for AoT we need to manually split universal packages +import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload'; + +import { AppModule, AppComponent } from './+app/app.module'; +import { SharedModule } from './+app/shared/shared.module'; +import { CacheService } from './+app/shared/cache.service'; + +// Will be merged into @angular/platform-browser in a later release +// see https://github.com/angular/angular/pull/12322 +import { Meta } from './angular2-meta'; + +// import * as LRU from 'modern-lru'; + +export function getLRU(lru?: any) { + // use LRU for node + // return lru || new LRU(10); + return lru || new Map(); +} +export function getRequest() { + // the request object only lives on the server + return { cookie: document.cookie }; +} +export function getResponse() { + // the response object is sent as the index.html and lives on the server + return {}; +} + + +// TODO(gdi2290): refactor into Universal +export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; + +@NgModule({ + bootstrap: [ AppComponent ], + imports: [ + // MaterialModule.forRoot() should be included first + UniversalModule, // BrowserModule, HttpModule, and JsonpModule are included + + FormsModule, + RouterModule.forRoot([], { useHash: false, preloadingStrategy: IdlePreload }), + + IdlePreloadModule.forRoot(), + SharedModule.forRoot(), + AppModule, + ], + providers: [ + { provide: 'isBrowser', useValue: isBrowser }, + { provide: 'isNode', useValue: isNode }, + + { provide: 'req', useFactory: getRequest }, + { provide: 'res', useFactory: getResponse }, + + { provide: 'LRU', useFactory: getLRU, deps: [] }, + + CacheService, + + Meta, + + // { provide: AUTO_PREBOOT, useValue: false } // turn off auto preboot complete + ] +}) +export class MainModule { + constructor(public cache: CacheService) { + // TODO(gdi2290): refactor into a lifecycle hook + this.doRehydrate(); + } + + doRehydrate() { + let defaultValue = {}; + let serverCache = this._getCacheValue(CacheService.KEY, defaultValue); + this.cache.rehydrate(serverCache); + } + + _getCacheValue(key: string, defaultValue: any): any { + // browser + const win: any = window; + if (win[UNIVERSAL_KEY] && win[UNIVERSAL_KEY][key]) { + let serverCache = defaultValue; + try { + serverCache = JSON.parse(win[UNIVERSAL_KEY][key]); + if (typeof serverCache !== typeof defaultValue) { + console.log('Angular Universal: The type of data from the server is different from the default value type'); + serverCache = defaultValue; + } + } catch (e) { + console.log('Angular Universal: There was a problem parsing the server data during rehydrate'); + serverCache = defaultValue; + } + return serverCache; + } else { + console.log('Angular Universal: UNIVERSAL_CACHE is missing'); + } + return defaultValue; + } +} diff --git a/src/client.aot.ts b/src/client.aot.ts new file mode 100644 index 00000000000..a34bae2eba5 --- /dev/null +++ b/src/client.aot.ts @@ -0,0 +1,36 @@ +// the polyfills must be the first thing imported +import 'angular2-universal-polyfills'; +import 'ts-helpers'; +import './__workaround.browser'; // temporary until 2.1.1 things are patched in Core + +// Angular 2 +import { enableProdMode } from '@angular/core'; +import { platformBrowser } from '@angular/platform-browser'; +import { bootloader } from '@angularclass/bootloader'; +// for AoT use platformBrowser +// import { platformUniversalDynamic } from 'angular2-universal/browser'; + +import { load as loadWebFont } from 'webfontloader'; + +// enable prod for faster renders +enableProdMode(); + +import { MainModuleNgFactory } from './browser.module.ngfactory'; + +export const platformRef = platformBrowser(); + +// on document ready bootstrap Angular 2 +export function main() { + // Load fonts async + // https://github.com/typekit/webfontloader#configuration + loadWebFont({ + google: { + families: ['Droid Sans'] + } + }); + + return platformRef.bootstrapModuleFactory(MainModuleNgFactory); +} + +// support async tag or hmr +bootloader(main); diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 00000000000..8b0a60ae1eb --- /dev/null +++ b/src/client.ts @@ -0,0 +1,34 @@ +// the polyfills must be the first thing imported +import 'angular2-universal-polyfills'; +import 'ts-helpers'; +import './__workaround.browser'; // temporary until 2.1.1 things are patched in Core + +// Angular 2 +import { enableProdMode } from '@angular/core'; +import { platformUniversalDynamic } from 'angular2-universal/browser'; +import { bootloader } from '@angularclass/bootloader'; + +import { load as loadWebFont } from 'webfontloader'; + +// enable prod for faster renders +// enableProdMode(); + +import { MainModule } from './browser.module'; + +export const platformRef = platformUniversalDynamic(); + +// on document ready bootstrap Angular 2 +export function main() { + // Load fonts async + // https://github.com/typekit/webfontloader#configuration + loadWebFont({ + google: { + families: ['Droid Sans'] + } + }); + + return platformRef.bootstrapModule(MainModule); +} + +// support async tag or hmr +bootloader(main); diff --git a/src/index.html b/src/index.html new file mode 100644 index 00000000000..4038a76f5fb --- /dev/null +++ b/src/index.html @@ -0,0 +1,23 @@ + + + + DSpace + + + + + + + + + + + + + + Loading DSpace ... + + + + + diff --git a/src/node.module.ts b/src/node.module.ts new file mode 100755 index 00000000000..8feb9d4f404 --- /dev/null +++ b/src/node.module.ts @@ -0,0 +1,73 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { UniversalModule, isBrowser, isNode } from 'angular2-universal/node'; // for AoT we need to manually split universal packages + +import { AppModule, AppComponent } from './+app/app.module'; +import { SharedModule } from './+app/shared/shared.module'; +import { CacheService } from './+app/shared/cache.service'; + +// Will be merged into @angular/platform-browser in a later release +// see https://github.com/angular/angular/pull/12322 +import { Meta } from './angular2-meta'; + +export function getLRU() { + return new Map(); +} +export function getRequest() { + return Zone.current.get('req') || {}; +} +export function getResponse() { + return Zone.current.get('res') || {}; +} + +// TODO(gdi2290): refactor into Universal +export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE'; + +@NgModule({ + bootstrap: [ AppComponent ], + imports: [ + // MaterialModule.forRoot() should be included first + UniversalModule, // BrowserModule, HttpModule, and JsonpModule are included + + FormsModule, + RouterModule.forRoot([], { useHash: false }), + + SharedModule.forRoot(), + AppModule, + ], + providers: [ + { provide: 'isBrowser', useValue: isBrowser }, + { provide: 'isNode', useValue: isNode }, + + { provide: 'req', useFactory: getRequest }, + { provide: 'res', useFactory: getResponse }, + + { provide: 'LRU', useFactory: getLRU, deps: [] }, + + CacheService, + + Meta, + ] +}) +export class MainModule { + constructor(public cache: CacheService) { + + } + + /** + * We need to use the arrow function here to bind the context as this is a gotcha + * in Universal for now until it's fixed + */ + universalDoDehydrate = (universalCache) => { + universalCache[CacheService.KEY] = JSON.stringify(this.cache.dehydrate()); + } + + /** + * Clear the cache after it's rendered + */ + universalAfterDehydrate = () => { + // comment out if LRU provided at platform level to be shared between each user + this.cache.clear(); + } +} diff --git a/src/server.aot.ts b/src/server.aot.ts new file mode 100644 index 00000000000..8f81f7cfc3e --- /dev/null +++ b/src/server.aot.ts @@ -0,0 +1,147 @@ +// the polyfills must be one of the first things imported in node.js. +// The only modules to be imported higher - node modules with es6-promise 3.x or other Promise polyfill dependency +// (rule of thumb: do it if you have zone.js exception that it has been overwritten) +// if you are including modules that modify Promise, such as NewRelic,, you must include them before polyfills +import 'angular2-universal-polyfills'; +import 'ts-helpers'; +import './__workaround.node'; // temporary until 2.1.1 things are patched in Core + +import * as fs from 'fs'; +import * as path from 'path'; +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import * as cookieParser from 'cookie-parser'; +import * as morgan from 'morgan'; +import * as mcache from 'memory-cache'; + +const { gzipSync } = require('zlib'); +const accepts = require('accepts'); +const { compressSync } = require('iltorb'); +const interceptor = require('express-interceptor'); + +// Angular 2 +import { enableProdMode } from '@angular/core'; +// Angular 2 Universal +import { createEngine } from 'angular2-express-engine'; + +// App +import { MainModuleNgFactory } from './node.module.ngfactory'; + +// Routes +import { routes } from './server.routes'; + +// enable prod for faster renders +enableProdMode(); + +const app = express(); +const ROOT = path.join(path.resolve(__dirname, '..')); + +// Express View +app.engine('.html', createEngine({ + precompile: false, // this needs to be false when using ngFactory + ngModule: MainModuleNgFactory, + providers: [ + // use only if you have shared state between users + // { provide: 'LRU', useFactory: () => new LRU(10) } + + // stateless providers only since it's shared + ] +})); +app.set('port', process.env.PORT || 3000); +app.set('views', __dirname); +app.set('view engine', 'html'); +app.set('json spaces', 2); + +app.use(cookieParser('Angular 2 Universal')); +app.use(bodyParser.json()); + +app.use(interceptor((req, res)=>({ + // don't compress responses with this request header + isInterceptable: () => (!req.headers['x-no-compression']), + intercept: ( body, send ) => { + const encodings = new Set(accepts(req).encodings()); + const bodyBuffer = new Buffer(body); + // url specific key for response cache + const key = '__response__' + req.originalUrl || req.url; + let output = bodyBuffer; + // check if cache exists + if (mcache.get(key) === null) { + // check for encoding support + if (encodings.has('br')) { + // brotli + res.setHeader('Content-Encoding', 'br'); + output = compressSync(bodyBuffer); + mcache.put(key, {output, encoding: 'br'}); + } else if (encodings.has('gzip')) { + // gzip + res.setHeader('Content-Encoding', 'gzip'); + output = gzipSync(bodyBuffer); + mcache.put(key, {output, encoding: 'gzip'}); + } + } else { + const { output, encoding } = mcache.get(key); + res.setHeader('Content-Encoding', encoding); + send(output); + } + send(output); + } +}))); + +const accessLogStream = fs.createWriteStream(ROOT + '/morgan.log', {flags: 'a'}) + +app.use(morgan('common', { + skip: (req, res) => res.statusCode < 400, + stream: accessLogStream +})); + +function cacheControl(req, res, next) { + // instruct browser to revalidate in 60 seconds + res.header('Cache-Control', 'max-age=60'); + next(); +} +// Serve static files +app.use('/assets', cacheControl, express.static(path.join(__dirname, 'assets'), {maxAge: 30})); +app.use(cacheControl, express.static(path.join(ROOT, 'dist/client'), {index: false})); + +// +///////////////////////// +// ** Example API +// Notice API should be in a separate process +import { serverApi, createTodoApi } from './backend/api'; +// Our API for demos only +app.get('/data.json', serverApi); +app.use('/api', createTodoApi()); + +function ngApp(req, res) { + res.render('index', { + req, + res, + // time: true, // use this to determine what part of your app is slow only in development + preboot: false, + baseUrl: '/', + requestUrl: req.originalUrl, + originUrl: `http://localhost:${ app.get('port') }` + }); +} + +/** + * use universal for specific routes + */ +app.get('/', ngApp); +routes.forEach(route => { + app.get(`/${route}`, ngApp); + app.get(`/${route}/*`, ngApp); +}); + + +app.get('*', function(req, res) { + res.setHeader('Content-Type', 'application/json'); + var pojo = { status: 404, message: 'No Content' }; + var json = JSON.stringify(pojo, null, 2); + res.status(404).send(json); +}); + +// Server +let server = app.listen(app.get('port'), () => { + console.log(`Listening on: http://localhost:${server.address().port}`); +}); diff --git a/src/server.routes.ts b/src/server.routes.ts new file mode 100644 index 00000000000..4c3246c7d51 --- /dev/null +++ b/src/server.routes.ts @@ -0,0 +1,17 @@ +/** + * Server-side routes. Only the listed routes support html5pushstate. + * Has to match client side routes. + * + * Index (/) route does not have to be listed here. + * + * @example + * export const routes: string[] = [ + * 'home', 'about' + * ]; + **/ +export const routes: string[] = [ + 'about', + 'home', + 'todo', + 'lazy', +]; diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 00000000000..351a82f7688 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,103 @@ +// the polyfills must be one of the first things imported in node.js. +// The only modules to be imported higher - node modules with es6-promise 3.x or other Promise polyfill dependency +// (rule of thumb: do it if you have zone.js exception that it has been overwritten) +// if you are including modules that modify Promise, such as NewRelic,, you must include them before polyfills +import 'angular2-universal-polyfills'; +import 'ts-helpers'; +import './__workaround.node'; // temporary until 2.1.1 things are patched in Core + +import * as path from 'path'; +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import * as cookieParser from 'cookie-parser'; +import * as morgan from 'morgan'; +import * as compression from 'compression'; + +// Angular 2 +import { enableProdMode } from '@angular/core'; +// Angular 2 Universal +import { createEngine } from 'angular2-express-engine'; + +// App +import { MainModule } from './node.module'; + +// Routes +import { routes } from './server.routes'; + +// enable prod for faster renders +enableProdMode(); + +const app = express(); +const ROOT = path.join(path.resolve(__dirname, '..')); + +// Express View +app.engine('.html', createEngine({ + ngModule: MainModule, + providers: [ + // use only if you have shared state between users + // { provide: 'LRU', useFactory: () => new LRU(10) } + + // stateless providers only since it's shared + ] +})); +app.set('port', process.env.PORT || 3000); +app.set('views', __dirname); +app.set('view engine', 'html'); +app.set('json spaces', 2); + +app.use(cookieParser('Angular 2 Universal')); +app.use(bodyParser.json()); +app.use(compression()); + +app.use(morgan('dev')); + +function cacheControl(req, res, next) { + // instruct browser to revalidate in 60 seconds + res.header('Cache-Control', 'max-age=60'); + next(); +} +// Serve static files +app.use('/assets', cacheControl, express.static(path.join(__dirname, 'assets'), {maxAge: 30})); +app.use(cacheControl, express.static(path.join(ROOT, 'dist/client'), {index: false})); + +// +///////////////////////// +// ** Example API +// Notice API should be in aseparate process +import { serverApi, createTodoApi } from './backend/api'; +// Our API for demos only +app.get('/data.json', serverApi); +app.use('/api', createTodoApi()); + +function ngApp(req, res) { + res.render('index', { + req, + res, + // time: true, // use this to determine what part of your app is slow only in development + preboot: false, + baseUrl: '/', + requestUrl: req.originalUrl, + originUrl: `http://localhost:${ app.get('port') }` + }); +} + +/** + * use universal for specific routes + */ +app.get('/', ngApp); +routes.forEach(route => { + app.get(`/${route}`, ngApp); + app.get(`/${route}/*`, ngApp); +}); + +app.get('*', function(req, res) { + res.setHeader('Content-Type', 'application/json'); + var pojo = { status: 404, message: 'No Content' }; + var json = JSON.stringify(pojo, null, 2); + res.status(404).send(json); +}); + +// Server +let server = app.listen(app.get('port'), () => { + console.log(`Listening on: http://localhost:${server.address().port}`); +}); diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 00000000000..dd39e59907f --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1,73 @@ +/* + * Custom Type Definitions + * When including 3rd party modules you also need to include the type definition for the module + * if they don't provide one within the module. You can try to install it with typings +typings install node --save + * If you can't find the type definition in the registry we can make an ambient definition in + * this file for now. For example +declare module "my-module" { + export function doesSomething(value: string): string; +} + * + * If you're prototying and you will fix the types later you can also declare it as type any + * +declare var assert: any; + * + * If you're importing a module that uses Node.js modules which are CommonJS you need to import as + * +import * as _ from 'lodash' + * You can include your type definitions in this file until you create one for the typings registry + * see https://github.com/typings/registry + * + */ + +// declare module '*'; // default type definitions for any for modules that are not found. +// caveat: if this is enabled and you do not have the proper module there may not be an error. +// suggestion: follow the pattern below with modern-lru which provides an alternative way to create an 'any' module. + +// for legacy tslint etc to understand +declare module 'modern-lru' { + let x: any; + export = x; +} + +declare var System: SystemJS; + +interface SystemJS { + import: (path?: string) => Promise; +} +// Extra variables that live on Global that will be replaced by webpack DefinePlugin +declare var ENV: string; +declare var HMR: boolean; +declare var Zone: {current: any}; +interface GlobalEnvironment { + ENV; + HMR; + SystemJS: SystemJS; + System: SystemJS; +} + +interface WebpackModule { + hot: { + data?: any, + idle: any, + accept(dependencies?: string | string[], callback?: (updatedDependencies?: any) => void): void; + decline(dependencies?: string | string[]): void; + dispose(callback?: (data?: any) => void): void; + addDisposeHandler(callback?: (data?: any) => void): void; + removeDisposeHandler(callback?: (data?: any) => void): void; + check(autoApply?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void; + apply(options?: any, callback?: (err?: Error, outdatedModules?: any[]) => void): void; + status(callback?: (status?: string) => void): void | string; + removeStatusHandler(callback?: (status?: string) => void): void; + }; +} + +interface WebpackRequire { + context(file: string, flag?: boolean, exp?: RegExp): any; +} + +// Extend typings +interface NodeRequire extends WebpackRequire {} +interface NodeModule extends WebpackModule {} +interface Global extends GlobalEnvironment {} diff --git a/tsconfig.aot.json b/tsconfig.aot.json new file mode 100644 index 00000000000..99eb05bc493 --- /dev/null +++ b/tsconfig.aot.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "commonjs", + "moduleResolution": "node", + "outDir": "dist", + "sourceMap": true, + "sourceRoot": "src", + "noEmitHelpers": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": ["es6", "dom"] + }, + "include": [ + "./src/**/*.module.ts", + "./src/*.d.ts" + ], + "angularCompilerOptions": { + "debug": false + }, + "compileOnSave": false, + "buildOnSave": false, + "atom": { "rewriteTsconfig": false } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..38cb58017bc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "commonjs", + "moduleResolution": "node", + "outDir": "dist", + "sourceMap": true, + "sourceRoot": "src", + "noEmitHelpers": true, + "target": "es5", + "typeRoots": [ + "node_modules/@types" + ], + "lib": ["es6", "dom"] + }, + "exclude": [ + "node_modules" + ], + "angularCompilerOptions": { + "debug": false + }, + "compileOnSave": false, + "buildOnSave": false, + "atom": { "rewriteTsconfig": false } +} diff --git a/webpack.config.ts b/webpack.config.ts new file mode 100644 index 00000000000..cbec9f48b5f --- /dev/null +++ b/webpack.config.ts @@ -0,0 +1,133 @@ +var webpack = require('webpack'); +var path = require('path'); +var clone = require('js.clone'); +var webpackMerge = require('webpack-merge'); + +export var commonPlugins = [ + new webpack.ContextReplacementPlugin( + // The (\\|\/) piece accounts for path separators in *nix and Windows + /angular(\\|\/)core(\\|\/)src(\\|\/)linker/, + root('./src'), + { + // your Angular Async Route paths relative to this root directory + } + ), + + // Loader options + new webpack.LoaderOptionsPlugin({ + + }), + +]; +export var commonConfig = { + // https://webpack.github.io/docs/configuration.html#devtool + devtool: 'source-map', + resolve: { + extensions: ['.ts', '.js', '.json'], + modules: [ root('node_modules') ] + }, + context: __dirname, + output: { + publicPath: '', + filename: '[name].bundle.js' + }, + module: { + rules: [ + // TypeScript + { test: /\.ts$/, use: ['awesome-typescript-loader', 'angular2-template-loader'] }, + { test: /\.html$/, use: 'raw-loader' }, + { test: /\.css$/, use: 'raw-loader' }, + { test: /\.json$/, use: 'json-loader' } + ], + }, + plugins: [ + // Use commonPlugins. + ] + +}; + +// Client. +export var clientPlugins = [ + +]; +export var clientConfig = { + target: 'web', + entry: './src/client', + output: { + path: root('dist/client') + }, + node: { + global: true, + crypto: 'empty', + __dirname: true, + __filename: true, + process: true, + Buffer: false + } +}; + + +// Server. +export var serverPlugins = [ + +]; +export var serverConfig = { + target: 'node', + entry: './src/server', // use the entry file of the node server if everything is ts rather than es5 + output: { + filename: 'index.js', + path: root('dist/server'), + libraryTarget: 'commonjs2' + }, + module: { + rules: [ + { test: /@angular(\\|\/)material/, use: "imports-loader?window=>global" } + ], + }, + externals: includeClientPackages( + /@angularclass|@angular|angular2-|ng2-|ng-|@ng-|angular-|@ngrx|ngrx-|@angular2|ionic|@ionic|-angular2|-ng2|-ng/ + ), + node: { + global: true, + crypto: true, + __dirname: true, + __filename: true, + process: true, + Buffer: true + } +}; + +export default [ + // Client + webpackMerge(clone(commonConfig), clientConfig, { plugins: clientPlugins.concat(commonPlugins) }), + + // Server + webpackMerge(clone(commonConfig), serverConfig, { plugins: serverPlugins.concat(commonPlugins) }) +]; + + + + +// Helpers +export function includeClientPackages(packages, localModule?: string[]) { + return function(context, request, cb) { + if (localModule instanceof RegExp && localModule.test(request)) { + return cb(); + } + if (packages instanceof RegExp && packages.test(request)) { + return cb(); + } + if (Array.isArray(packages) && packages.indexOf(request) !== -1) { + return cb(); + } + if (!path.isAbsolute(request) && request.charAt(0) !== '.') { + return cb(null, 'commonjs ' + request); + } + return cb(); + }; +} + +export function root(args) { + args = Array.prototype.slice.call(arguments, 0); + return path.join.apply(path, [__dirname].concat(args)); +} diff --git a/webpack.prod.config.ts b/webpack.prod.config.ts new file mode 100644 index 00000000000..a0f1b663047 --- /dev/null +++ b/webpack.prod.config.ts @@ -0,0 +1,178 @@ +const webpack = require('webpack'); +const path = require('path'); +const clone = require('js.clone'); +const webpackMerge = require('webpack-merge'); +const V8LazyParseWebpackPlugin = require('v8-lazy-parse-webpack-plugin'); +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +import webpackConfig, { root, includeClientPackages } from './webpack.config'; +// const CompressionPlugin = require('compression-webpack-plugin'); + + +export const commonPlugins = [ + new V8LazyParseWebpackPlugin(), + + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': JSON.stringify('production'), + 'AOT': true + } + }), + + // Loader options + new webpack.LoaderOptionsPlugin({ + minimize: true, + debug: false + }), + + new webpack.NormalModuleReplacementPlugin( + /facade(\\|\/)async/, + root('node_modules/@angular/core/src/facade/async.js') + ), + new webpack.NormalModuleReplacementPlugin( + /facade(\\|\/)collection/, + root('node_modules/@angular/core/src/facade/collection.js') + ), + new webpack.NormalModuleReplacementPlugin( + /facade(\\|\/)errors/, + root('node_modules/@angular/core/src/facade/errors.js') + ), + new webpack.NormalModuleReplacementPlugin( + /facade(\\|\/)lang/, + root('node_modules/@angular/core/src/facade/lang.js') + ), + new webpack.NormalModuleReplacementPlugin( + /facade(\\|\/)math/, + root('node_modules/@angular/core/src/facade/math.js') + ), + +]; +export const commonConfig = { + output: { + filename: '[name].bundle.js', + chunkFilename: '[chunkhash].js' + }, +}; + +// Client. +export const clientPlugins = [ + new BundleAnalyzerPlugin({ + analyzerMode: 'disabled', // change it to `server` to view bundle stats + reportFilename: 'report.html', + generateStatsFile: true, + statsFilename: 'stats.json', + }), + // To use gzip, you can run 'npm install compression-webpack-plugin --save-dev' + // add 'var CompressionPlugin = require("compression-webpack-plugin");' on the top + // and comment out below codes + // + // new CompressionPlugin({ + // asset: "[path].gz[query]", + // algorithm: "gzip", + // test: /\.js$|\.css$|\.html$/, + // threshold: 10240, + // minRatio: 0.8 + // }), + + new webpack.optimize.UglifyJsPlugin({ + // beautify: true, + // mangle: false, + output: { + comments: false + }, + compress: { + warnings: false, + conditionals: true, + unused: true, + comparisons: true, + sequences: true, + dead_code: true, + evaluate: true, + if_return: true, + join_vars: true, + negate_iife: false // we need this for lazy v8 + }, + sourceMap: true + }), + + new webpack.NormalModuleReplacementPlugin( + /@angular(\\|\/)upgrade/, + root('empty.js') + ), + // problem with platformUniversalDynamic on the server/client + new webpack.NormalModuleReplacementPlugin( + /@angular(\\|\/)compiler/, + root('empty.js') + ), + new webpack.NormalModuleReplacementPlugin( + /@angular(\\|\/)platform-browser-dynamic/, + root('empty.js') + ), + new webpack.NormalModuleReplacementPlugin( + /dom(\\|\/)debug(\\|\/)ng_probe/, + root('empty.js') + ), + new webpack.NormalModuleReplacementPlugin( + /dom(\\|\/)debug(\\|\/)by/, + root('empty.js') + ), + new webpack.NormalModuleReplacementPlugin( + /src(\\|\/)debug(\\|\/)debug_node/, + root('empty.js') + ), + new webpack.NormalModuleReplacementPlugin( + /src(\\|\/)debug(\\|\/)debug_renderer/, + root('empty.js') + ), + + // Waiting for https://github.com/ampedandwired/html-webpack-plugin/issues/446 + // new webpack.optimize.AggressiveSplittingPlugin({ + // minSize: 30000, + // maxSize: 250000 + // }), + +]; +export const clientConfig = { + entry: './src/client.aot', + recordsOutputPath: root('webpack.records.json') +}; + +// Server. + +export const serverPlugins = [ + new webpack.optimize.UglifyJsPlugin({ + // beautify: true, + mangle: false, // to ensure process.env still works + output: { + comments: false + }, + compress: { + warnings: false, + conditionals: true, + unused: true, + comparisons: true, + sequences: true, + dead_code: true, + evaluate: true, + if_return: true, + join_vars: true, + negate_iife: false // we need this for lazy v8 + }, + sourceMap: true + }), +]; +export const serverConfig = { + entry: './src/server.aot', + output: { + filename: 'index.js', + chunkFilename: '[id].bundle.js', + crossOriginLoading: false + }, +}; + +export default [ + // Client + webpackMerge(webpackConfig[0], clone(commonConfig), clientConfig, {plugins: webpackConfig[0].plugins.concat(commonPlugins, clientPlugins) }), + + // Server + webpackMerge(webpackConfig[1], clone(commonConfig), serverConfig, {plugins: webpackConfig[1].plugins.concat(commonPlugins, serverPlugins) }) +];