Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes 04-05 – Frontend, isomorphic bundles, hot reloading #1

Open
wants to merge 20 commits into
base: 04-unit-and-integration-tests
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
@@ -1,5 +1,9 @@
node_modules
lib
service/www/static/
service/www/webpack-stats.json
coverage
.nyc_output
.vscode
.isomorphic-loader-config.json
.idea
17 changes: 17 additions & 0 deletions docs/05-frontend-and-isomorphic-bundles.md
@@ -0,0 +1,17 @@
## 05 – Frontend, Isomorphic Bundles, Hot Reloading & more

This branch introduces a lot of new NPM scripts and tools (webpack, nodemon, isomorphic-loader, awesome-typescript-loader, core-js and some more) to the codebase that will be used during the development flow. Use them as followed:

1. `npm start` – Changed! It now uses nodemon to run `npm run server` and listens to source code changes. On changes it restarts.
2. `npm run server` – This is the previous `npm start`, not ran by `npm start`. Uses `ts-node` to "directly run" the server entrypoint file.
3. `npm run build:client` – It does the Webpack build for the frontend
4. `npm run build:server` – Creates the build of the whole application inside `./lib` using the TypeScript compiler (`tsc`)
5. `npm run build` – Combines `npm run build:client` and `npm run build:server` for a complete build of the application.

And the other NPM scripts being available at this point:

1. `npm run prettier:check` and `prettier:write` checks and fixes linting issues using [Prettier](https://prettier.io/)
2. `npm run test:unit` runs unit tests (`/**/*Spec.ts`) using Mocha
3. `npm run test:integration` runs integration tests (`test/integration/**/*Test.ts`) using Mocha
4. `npm run test` runs Prettier check, unit as well as integration tests in sequence
5. `npm run client:statistics` uses [webpack-bundle-analyzer](https://www.npmjs.com/package/webpack-bundle-analyzer) to visualise the weight of code and dependencies of the application
6,021 changes: 5,488 additions & 533 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 25 additions & 3 deletions package.json
Expand Up @@ -8,12 +8,18 @@
"npm": ">=6.4.1"
},
"scripts": {
"start": "ts-node service/server/index.ts",
"start": "nodemon --watch service -e ts,tsx --ignore 'service/frontend' --ignore '*Spec.ts*' --ignore '*Test.ts*' --exec 'npm run server'",
"server": "ts-node service/server/index.ts",
"prettier:check": "prettier --check service/**/*.{ts,tsx,jsx}",
"prettier:write": "prettier --write service/**/*.{ts,tsx,jsx}",
"test": "npm run prettier:check && npm run test:unit && npm audit",
"test:unit": "_mocha 'service/**/*Spec.ts' --opts service/mocha.opts",
"test:integration": "_mocha 'test/integration/**/*Test.ts' --opts service/mocha.opts"
"test:integration": "_mocha 'test/integration/**/*Test.ts' --opts service/mocha.opts",
"copy-service-assets": "cd service && find . -name '*.json' | cpio -pdm ../lib",
"build": "npm run build:server && npm run build:client",
"build:server": "npm run copy-service-assets && tsc",
"build:client": "NODE_ENV=production webpack -p",
"client:statistics": "NODE_ENV=production webpack --profile --json > service/www/webpack-stats.json && webpack-bundle-analyzer service/www/webpack-stats.json"
},
"dependencies": {
"express": "^4.16.2",
Expand All @@ -22,33 +28,49 @@
"http-status-codes": "^1.3.0",
"connect-datadog": "^0.0.6",
"hot-shots": "^4.7.0",
"fs-extra": "7.0.1",
"supertest": "^3.0.0",
"tslib": "^1.9.3"
},
"devDependencies": {
"@types/chai": "4.1.7",
"@types/chai-as-promised": "7.1.0",
"@types/chai-string": "1.4.1",
"@types/fs-extra": "5.0.0",
"@types/chai-subset": "1.3.1",
"@types/compression": "0.0.35",
"@types/cookie-parser": "1.4.1",
"@types/express": "4.11.1",
"@types/supertest": "2.0.4",
"@types/mocha": "5.2.5",
"@types/sinon-chai": "3.2.2",
"@types/webpack": "^4.4.22",
"@types/webpack-dev-middleware": "^2.0.2",
"@types/webpack-hot-middleware": "^2.16.4",
"awesome-typescript-loader": "^5.2.1",
"chai": "4.2.0",
"chai-as-promised": "7.1.1",
"chai-shallow-deep-equal": "1.4.6",
"chai-string": "1.5.0",
"chai-subset": "1.6.0",
"expressmocks": "^0.1.3",
"nodemon": "^1.19.0",
"mocha": "5.2.0",
"isomorphic-loader": "^2.0.2",
"mocha-better-spec-reporter": "3.1.0",
"mini-css-extract-plugin": "^0.5.0",
"prettier": "^1.9.2",
"sinon": "7.2.2",
"sinon-chai": "3.3.0",
"ts-node": "6.1.1",
"typescript": "3.1.1"
"typescript": "3.1.1",
"core-js": "2.6.1",
"webpack": "^4.30.0",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.2.0",
"webpack-dev-middleware": "^3.4.0",
"webpack-hot-middleware": "^2.24.3",
"webpack-merge": "4.1.5"
},
"author": "Jonas Verhoelen <jonas.verhoelen@codecentric.de>",
"repository": {
Expand Down
23 changes: 23 additions & 0 deletions resources/views/index.ejs
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<base href="/">
<meta name="msapplication-tap-highlight" content="no"/>
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"/>
<title>My App</title>
<% for (var i = 0; i < cssFiles.length; i++) { %>
<link rel="stylesheet" href="<%= cssFiles[i] %>" />
<% } %>
<style type="text/css">
.js #app { display: none; }
</style>
</head>
<body>
<div id="app"></div>
<script type="text/javascript">
window.config = <%- JSON.stringify(config) %>
</script>
<script type="text/javascript" src="/static/app-bundle.js" async></script>
</body>
</html>
4 changes: 4 additions & 0 deletions service/frontend/index.ts
@@ -0,0 +1,4 @@
const welcomePhrases = window.config.welcomePhrases

const app: HTMLElement = document.getElementById('app')!
app.innerHTML = welcomePhrases.map(phrase => `<h2>${phrase}</h2>`).join('')
3 changes: 2 additions & 1 deletion service/server/Application.ts
Expand Up @@ -2,6 +2,7 @@ import { ExpressServer } from './ExpressServer'
import { CatEndpoints } from './cats/CatEndpoints'
import { CatService } from './cats/CatService'
import { CatRepository } from './cats/CatRepository'
import { Environment } from './Environment'

/**
* Wrapper around the Node process, ExpressServer abstraction and complex dependencies such as services that ExpressServer needs.
Expand All @@ -13,7 +14,7 @@ export class Application {
const requestServices = { catService }
const expressServer = new ExpressServer(new CatEndpoints(), requestServices)

await expressServer.setup(8000)
await expressServer.setup(Environment.getPort())
Application.handleExit(expressServer)

return expressServer
Expand Down
17 changes: 17 additions & 0 deletions service/server/Environment.ts
@@ -0,0 +1,17 @@
export class Environment {
public static isLocal(): boolean {
return Environment.getStage() === 'local'
}

public static isProd(): boolean {
return Environment.getStage() === 'prod'
}

public static getStage(): string {
return process.env.STAGE || 'local'
}

public static getPort(): number {
return process.env.PORT as any || 8000
}
}
69 changes: 68 additions & 1 deletion service/server/ExpressServer.ts
@@ -1,6 +1,7 @@
import * as express from 'express'
import { Express } from 'express'
import { Express, NextFunction, Response, Request } from 'express'
import { Server } from 'http'
import * as fse from 'fs-extra'
import * as compress from 'compression'
import * as bodyParser from 'body-parser'
import * as cookieParser from 'cookie-parser'
Expand All @@ -10,6 +11,8 @@ import DatadogStatsdMiddleware from './middlewares/DatadogStatsdMiddleware'
import { CatEndpoints } from './cats/CatEndpoints'
import { RequestServices } from './types/CustomRequest'
import { addServicesToRequest } from './middlewares/ServiceDependenciesMiddleware'
import { Environment } from './Environment'
import { FrontendContext } from '../shared/FrontendContext'

/**
* Abstraction around the raw Express.js server and Nodes' HTTP server.
Expand All @@ -18,15 +21,19 @@ import { addServicesToRequest } from './middlewares/ServiceDependenciesMiddlewar
*/
export class ExpressServer {
private server?: Express
private cssFiles?: string[]
private httpServer?: Server

constructor(private catEndpoints: CatEndpoints, private requestServices: RequestServices) {}

public async setup(port: number) {
const server = express()
this.setupStandardMiddlewares(server)
this.applyWebpackDevMiddleware(server)
this.setupTelemetry(server)
this.setupServiceDependencies(server)
this.configureEjsTemplates(server)
this.configureFrontendPages(server)
this.configureApiEndpoints(server)

this.httpServer = this.listen(server, port)
Expand All @@ -48,6 +55,11 @@ export class ExpressServer {
server.use(compress())
}

private configureEjsTemplates(server: Express) {
server.set('views', [ 'resources/views' ])
server.set('view engine', 'ejs')
}

private setupTelemetry(server: Express) {
DatadogStatsdMiddleware.applyTo(server, {
targetHost: 'https://datadog.mycompany.com',
Expand All @@ -61,6 +73,61 @@ export class ExpressServer {
server.use(servicesMiddleware)
}

private configureFrontendPages(server: Express) {
this.prepareAssets()
this.configureStaticAssets(server)

const context: FrontendContext = {
cssFiles: this.cssFiles,
config: {
welcomePhrases: [ 'Bienvenue', 'Welcome', 'Willkommen', 'Welkom', 'Hoş geldin', 'Benvenuta', 'Bienvenido' ]
}
}
const renderPage = (template: string) => async (req: Request, res: Response, _: NextFunction) => {
res.type('text/html').render(template, context)
}

server.get('/', noCache, renderPage('index'))
}

private configureStaticAssets(server: Express) {
if (Environment.isProd()) {
server.use([/(.*)\.js\.map$/, '/'], express.static('www/'))
} else {
server.use('/', express.static('www/'))
}

server.use('/', express.static('resources/img/'))
}

private applyWebpackDevMiddleware(server: Express) {
if (Environment.isLocal()) {
const config = require('../../webpack.config.js')
const compiler = require('webpack')(config)

const webpackDevMiddleware = require('webpack-dev-middleware')
server.use(webpackDevMiddleware(compiler, {
hot: true,
publicPath: config.output.publicPath,
compress: true,
host: 'localhost',
port: Environment.getPort()
}))

const webpackHotMiddleware = require('webpack-hot-middleware')
server.use(webpackHotMiddleware(compiler))
}
}

private async prepareAssets() {
if (Environment.isLocal()) {
this.cssFiles = []
} else {
const isomorphicAssets: any = JSON.parse(await fse.readFile('www/static/media/isomorphic-assets.json', 'utf-8'))
this.cssFiles = isomorphicAssets.chunks.app.filter((path: string) => path.endsWith('.css'))
}
}

private configureApiEndpoints(server: Express) {
server.get('/api/cat', noCache, this.catEndpoints.getAllCats)
server.get('/api/statistics/cat', noCache, this.catEndpoints.getCatsStatistics)
Expand Down
8 changes: 8 additions & 0 deletions service/shared/FrontendContext.d.ts
@@ -0,0 +1,8 @@
export interface FrontendConfig {
welcomePhrases: string[]
}

export interface FrontendContext {
cssFiles?: string[]
config: FrontendConfig
}
7 changes: 7 additions & 0 deletions service/shared/Window.d.ts
@@ -0,0 +1,7 @@
import { FrontendConfig } from './FrontendContext'

declare global {
interface Window {
config: FrontendConfig
}
}
25 changes: 25 additions & 0 deletions tsconfig-frontend.json
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"outDir": "./lib",
"allowJs": true,
"target": "es5",
"jsx": "react",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noUnusedParameters": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"importHelpers": true,
"lib": [ "es7", "dom" ]
},
"include": [
"./service/frontend/**/*",
"./service/shared/**/*"
],
"exclude": [
"./service/**/*Spec.*"
]
}