commit a5735569633c9f7b947ed33d11540c39d8b0e077 Author: Art Lowel Date: Wed Nov 30 15:13:29 2016 +0100 initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..f1cc3ad329 --- /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 0000000000..91729af70e --- /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 0000000000..f2449acd54 --- /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 0000000000..ccdda087eb --- /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 0000000000..50352981ab --- /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 0000000000..1762bb6b8b --- /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 0000000000..b8ab065d5a --- /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 0000000000..97a836fbb5 --- /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 0000000000..aa348c0aec --- /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 0000000000..2a2f493b2f --- /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 0000000000..08f97f936f --- /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 0000000000..32b2461a3c --- /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 0000000000..62a0799f2f --- /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 0000000000..17b877f70b --- /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 0000000000..75663111f7 --- /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 0000000000..1d18f3e061 --- /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 0000000000..3d8028cbb7 --- /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 0000000000..8391fc4f6a --- /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 0000000000..98b3a39693 --- /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 0000000000..0fb8ee061c --- /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 0000000000..5f84c3eafa --- /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 0000000000..11d7dd66b9 --- /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 0000000000..9470429641 --- /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 0000000000..be947e59f2 --- /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 0000000000..97617d346a --- /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 0000000000..4aa6b5fac2 --- /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 0000000000..5c5aa2611a --- /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 0000000000..87780632e5 --- /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 0000000000..7d44a2b223 --- /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 0000000000..a99fcdea09 --- /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 0000000000..026f505fa3 --- /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 0000000000..ed39c8d319 --- /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 0000000000..f961d9e4fe --- /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 0000000000..afea237f93 Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/backend/api.ts b/src/backend/api.ts new file mode 100644 index 0000000000..3acf45b619 --- /dev/null +++ b/src/backend/api.ts @@ -0,0 +1,106 @@ +var util = require('util'); +var {Router} = require('express'); + +// Our API for demos only +import {fakeDataBase} from './db'; +import {fakeDemoRedisCache} from './cache'; + +// you would use cookies/token etc +var USER_ID = 'f9d98cf1-1b96-464e-8755-bcc2a5c09077'; // hardcoded as an example + +// Our API for demos only +export function serverApi(req, res) { + let key = USER_ID + '/data.json'; + let cache = fakeDemoRedisCache.get(key); + if (cache !== undefined) { + console.log('/data.json Cache Hit'); + return res.json(cache); + } + console.log('/data.json Cache Miss'); + + fakeDataBase.get() + .then(data => { + 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 0000000000..490a35612d --- /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 0000000000..3ba8ea3d44 --- /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 0000000000..a8ec09c73a --- /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 0000000000..a34bae2eba --- /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 0000000000..8b0a60ae1e --- /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 0000000000..4038a76f5f --- /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 0000000000..8feb9d4f40 --- /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 0000000000..8f81f7cfc3 --- /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 0000000000..4c3246c7d5 --- /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 0000000000..351a82f768 --- /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 0000000000..dd39e59907 --- /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 0000000000..99eb05bc49 --- /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 0000000000..38cb58017b --- /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 0000000000..cbec9f48b5 --- /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 0000000000..a0f1b66304 --- /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) }) +];