diff --git a/.angular-cli.json b/.angular-cli.json index db54d3b..49d9379 100644 --- a/.angular-cli.json +++ b/.angular-cli.json @@ -29,6 +29,31 @@ "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } + }, + { + "platform": "server", + "root": "src", + "outDir": "dist-server", + "assets": [ + "assets", + "favicon.ico" + ], + "index": "index.html", + "main": "main.server.ts", + "test": "test.ts", + "tsconfig": "tsconfig.server.json", + "testTsconfig": "tsconfig.spec.json", + "prefix": "app", + "styles": [ + "styles/bootstrap.scss" + ], + "scripts": [ + ], + "environmentSource": "environments/environment.ts", + "environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" + } } ], "e2e": { diff --git a/package-lock.json b/package-lock.json index ca9a427..03892e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,6 +154,15 @@ "tslib": "1.8.0" } }, + "@angular/http": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.0.1.tgz", + "integrity": "sha1-NQy99jz6yJOWE9dT/wce1YpgVhs=", + "dev": true, + "requires": { + "tslib": "1.8.0" + } + }, "@angular/platform-browser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.0.0.tgz", @@ -170,6 +179,16 @@ "tslib": "1.8.0" } }, + "@angular/platform-server": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-5.0.1.tgz", + "integrity": "sha1-AFpk62V+Zw0mX9+ulHPxmnZPhzc=", + "requires": { + "domino": "1.0.30", + "tslib": "1.8.0", + "xhr2": "0.1.4" + } + }, "@angular/router": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.0.0.tgz", @@ -362,6 +381,16 @@ "tree-kill": "1.2.0" } }, + "@nguniversal/express-engine": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@nguniversal/express-engine/-/express-engine-5.0.0-beta.5.tgz", + "integrity": "sha1-PLwPt/koAS1hJAaBYxWB+wrTJ/U=" + }, + "@nguniversal/module-map-ngfactory-loader": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@nguniversal/module-map-ngfactory-loader/-/module-map-ngfactory-loader-5.0.0-beta.5.tgz", + "integrity": "sha1-2GSX5wT3AuGhi7yrXdZYYFeUH6U=" + }, "@schematics/angular": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-0.1.0.tgz", @@ -2947,6 +2976,11 @@ "domelementtype": "1.3.0" } }, + "domino": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/domino/-/domino-1.0.30.tgz", + "integrity": "sha512-ikq8WiDSkICdkElud317F2Sigc6A3EDpWsxWBwIZqOl95km4p/Vc9Rj98id7qKgsjDmExj0AVM7JOd4bb647Xg==" + }, "domutils": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", @@ -13267,6 +13301,31 @@ "utf8-byte-length": "1.0.4" } }, + "ts-loader": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-3.1.1.tgz", + "integrity": "sha512-AQmLFSIgTiR8AlS5BxqvoHpZ3OUTwHHuDZTAZ2KcKsYRz/yANGeQn4Se/DCQ4cn1/eVvN37f/caVW4+kUPNNHw==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "enhanced-resolve": "3.4.1", + "loader-utils": "1.1.0", + "semver": "5.4.1" + }, + "dependencies": { + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + } + } + }, "ts-node": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", @@ -14662,6 +14721,11 @@ "os-homedir": "1.0.2" } }, + "xhr2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.1.4.tgz", + "integrity": "sha1-f4dliEdxbbUCYyOBL4GMras4el8=" + }, "xml-char-classes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/xml-char-classes/-/xml-char-classes-1.0.0.tgz", diff --git a/package.json b/package.json index 6098fca..6207c0e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,12 @@ "commitmsg": "node ./build/commit-msg.js", "prepush": "npm test", "prettier-watch": "onchange '**/*.ts' -- prettier --write --single-quote --trailing-comma=all {{changed}}", - "prettier": "prettier --parser typescript --single-quote --trailing-comma=all --write \"./**/*.ts\"" + "prettier": "prettier --parser typescript --single-quote --trailing-comma=all --write \"./**/*.ts\"", + "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server", + "serve:ssr": "node dist-server/server.js", + "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false", + "webpack:server": "webpack --config webpack.server.config.js --progress --colors", + "ssr": "npm run build:ssr && npm run serve:ssr" }, "lint-staged": { "*.{js,css,scss,ts}": [ @@ -44,7 +49,10 @@ "@angular/forms": "^5.0.0", "@angular/platform-browser": "^5.0.0", "@angular/platform-browser-dynamic": "^5.0.0", + "@angular/platform-server": "^5.0.1", "@angular/router": "^5.0.0", + "@nguniversal/express-engine": "^5.0.0-beta.5", + "@nguniversal/module-map-ngfactory-loader": "^5.0.0-beta.5", "bootstrap-sass": "^3.3.7", "classlist.js": "^1.1.20150312", "core-js": "^2.5.1", @@ -63,6 +71,7 @@ "devDependencies": { "@angular/cli": "^1.5.0", "@angular/compiler-cli": "^5.0.0", + "@angular/http": "^5.0.1", "@compodoc/compodoc": "^1.0.0-beta.10", "@types/jasmine": "^2.5.54", "@types/jasminewd2": "^2.0.3", @@ -91,6 +100,7 @@ "protractor": "^5.1.2", "stubby": "^4.0.0", "sw-precache": "^5.2.0", + "ts-loader": "^3.1.1", "ts-node": "^3.3.0", "tslint": "~5.7.0", "tslint-config-prettier": "^1.6.0", diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..2ee1bfb --- /dev/null +++ b/server.ts @@ -0,0 +1,57 @@ +// These are important and needed before anything else +import 'zone.js/dist/zone-node'; +import 'reflect-metadata'; + +import { enableProdMode } from '@angular/core'; + +import * as express from 'express'; +import { join } from 'path'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// Express server +const app = express(); + +const PORT = process.env.PORT || 4000; +const CLIENT_DIST_FOLDER = join(process.cwd(), 'dist'); + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { + AppServerModuleNgFactory, + LAZY_MODULE_MAP, +} = require('./dist-server/main.bundle'); + +// Express Engine +import { ngExpressEngine } from '@nguniversal/express-engine'; +// Import module map for lazy loading +import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; + +app.engine( + 'html', + ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [provideModuleMap(LAZY_MODULE_MAP)], + }), +); + +app.set('view engine', 'html'); +app.set('views', join(CLIENT_DIST_FOLDER, 'browser')); + +// TODO: implement data requests securely +app.get('/api/*', (req, res) => { + res.status(404).send('data requests are not supported'); +}); + +// Server static files from /browser +app.get('*.*', express.static(CLIENT_DIST_FOLDER)); + +// All regular routes use the Universal engine +app.get('*', (req, res) => { + res.render(join(CLIENT_DIST_FOLDER, 'index.html'), { req }); +}); + +// Start up the Node server +app.listen(PORT, () => { + console.log(`Node server listening on http://localhost:${PORT}`); +}); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ab24f3b..4e4d12c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,6 +12,7 @@ import { SharedModule } from './shared/shared.module'; declarations: [AppComponent], imports: [ BrowserModule, + BrowserModule.withServerTransition({ appId: 'rebirth-admin' }), BrowserAnimationsModule, CoreModule, SharedModule, diff --git a/src/app/app.server.module.ts b/src/app/app.server.module.ts new file mode 100644 index 0000000..cdb1a40 --- /dev/null +++ b/src/app/app.server.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; + +import { AppModule } from './app.module'; +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [AppModule, ServerModule, ModuleMapLoaderModule], + providers: [ + // Add universal-only providers here + ], + bootstrap: [AppComponent], +}) +export class AppServerModule {} diff --git a/src/main.server.ts b/src/main.server.ts new file mode 100644 index 0000000..d7c01cd --- /dev/null +++ b/src/main.server.ts @@ -0,0 +1 @@ +export { AppServerModule } from './app/app.server.module'; diff --git a/src/tsconfig.app.json b/src/tsconfig.app.json index 39ba8db..c730fcb 100644 --- a/src/tsconfig.app.json +++ b/src/tsconfig.app.json @@ -8,6 +8,7 @@ }, "exclude": [ "test.ts", - "**/*.spec.ts" + "**/*.spec.ts", + "test-utils/**/*.ts" ] } diff --git a/src/tsconfig.server.json b/src/tsconfig.server.json new file mode 100644 index 0000000..6610bd5 --- /dev/null +++ b/src/tsconfig.server.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "baseUrl": "./", + "module": "commonjs", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts", + "test-utils/**/*.ts" + ], + "angularCompilerOptions": { + "entryModule": "app/app.server.module#AppServerModule" + } +} diff --git a/webpack.server.config.js b/webpack.server.config.js new file mode 100644 index 0000000..8bd26b7 --- /dev/null +++ b/webpack.server.config.js @@ -0,0 +1,33 @@ +const path = require('path'); +const webpack = require('webpack'); + +module.exports = { + entry: {server: './server.ts'}, + resolve: {extensions: ['.js', '.ts']}, + target: 'node', + // this makes sure we include node_modules and other 3rd party libraries + externals: [/(node_modules|main\..*\.js)/], + output: { + path: path.join(__dirname, 'dist-server'), + filename: '[name].js' + }, + module: { + rules: [ + {test: /\.ts$/, loader: 'ts-loader'} + ] + }, + plugins: [ + // Temporary Fix for issue: https://github.com/angular/angular/issues/11580 + // for "WARNING Critical dependency: the request of a dependency is an expression" + new webpack.ContextReplacementPlugin( + /(.+)?angular(\\|\/)core(.+)?/, + path.join(__dirname, 'src'), // location of your src + {} // a map of your routes + ), + new webpack.ContextReplacementPlugin( + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + {} + ) + ] +}