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 19 commits into
base: 04-unit-and-integration-tests
from
Open
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -1,5 +1,9 @@
node_modules
lib
service/www/static/
service/www/webpack-stats.json
coverage
.nyc_output
.vscode
.isomorphic-loader-config.json
.idea
@@ -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

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -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",
@@ -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": {
@@ -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>
@@ -0,0 +1,4 @@
const welcomePhrases = window.config.welcomePhrases

const app: HTMLElement = document.getElementById('app')!
app.innerHTML = welcomePhrases.map(phrase => `<h2>${phrase}</h2>`).join('')
@@ -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.
@@ -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
@@ -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
}
}
@@ -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'
@@ -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.
@@ -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)
@@ -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',
@@ -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)
@@ -0,0 +1,8 @@
export interface FrontendConfig {
welcomePhrases: string[]
}

export interface FrontendContext {
cssFiles?: string[]
config: FrontendConfig
}
@@ -0,0 +1,7 @@
import { FrontendConfig } from './FrontendContext'

declare global {
interface Window {
config: FrontendConfig
}
}
@@ -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.*"
]
}
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.