Skip to content

Commit 912d656

Browse files
author
Patryk Mrukot
committed
feat(roc-package-web-app): Upgrade package to use Koa@2
BREAKING CHANGE: Middlewares need to be defined as an async functions instead of generators.
1 parent ab8b3d5 commit 912d656

File tree

8 files changed

+200
-30
lines changed

8 files changed

+200
-30
lines changed

packages/roc-package-web-app/package.json

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,21 @@
2626
"dependencies": {
2727
"config": "~1.16.0",
2828
"debug": "~2.2.0",
29-
"koa": "~1.1.1",
30-
"koa-accesslog": "~0.0.2",
31-
"koa-add-trailing-slashes": "~1.1.0",
32-
"koa-compressor": "~1.0.3",
29+
"koa": "~2.4.1",
30+
"koa-add-trailing-slashes": "~2.0.1",
31+
"koa-compress": "~2.0.0",
3332
"koa-conditional-get": "~1.0.3",
34-
"koa-errors": "~1.0.1",
35-
"koa-etag": "~2.0.0",
36-
"koa-favicon": "~1.2.0",
37-
"koa-helmet": "~0.3.0",
38-
"koa-logger": "~1.3.0",
39-
"koa-lowercase-path": "~1.0.0",
40-
"koa-normalize-path": "~1.0.0",
41-
"koa-remove-trailing-slashes": "~1.0.0",
42-
"koa-static": "~2.0.0",
33+
"koa-error": "~3.1.1",
34+
"koa-etag": "~3.0.0",
35+
"koa-favicon": "~2.0.0",
36+
"koa-helmet": "~3.3.0",
37+
"koa-logger": "~3.1.0",
38+
"koa-lowercase-path": "~2.0.0",
39+
"koa-normalize-path": "~2.0.0",
40+
"koa-remove-trailing-slashes": "~2.0.0",
41+
"koa-static": "~4.0.2",
4342
"lodash": "4.13.1",
43+
"moment": "~2.20.1",
4444
"roc": "^1.0.0-rc.23",
4545
"roc-package-webpack-node": "^1.0.0",
4646
"roc-package-webpack-web": "^1.0.0"

packages/roc-package-web-app/src/app/createServer.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import http from 'http';
33
import https from 'https';
44

55
import debug from 'debug';
6-
import koa from 'koa';
6+
import Koa from 'koa';
77
import serve from 'koa-static';
88
import addTrailingSlash from 'koa-add-trailing-slashes';
99
import normalizePath from 'koa-normalize-path';
1010
import lowercasePath from 'koa-lowercase-path';
1111
import removeTrailingSlash from 'koa-remove-trailing-slashes';
1212
import { merge, getSettings, getAbsolutePath } from 'roc';
1313

14-
import basepathSupport from './basepathSupport';
14+
import basepathSupport from './middlewares/basepathSupport';
1515

1616
/**
1717
* Creates a server instance.
@@ -41,15 +41,15 @@ export default function createServer(options = {}, beforeUserMiddlewares = [], {
4141
const logger = debug('roc:server');
4242
logger.log = console.info.bind(console);
4343

44-
const server = koa();
44+
const server = new Koa();
4545
const runtimeSettings = merge(getSettings('runtime'), options);
4646

4747
// Add support for rocPath
4848
server.use(basepathSupport(rocPath));
4949

5050
if (useDefaultKoaMiddlewares) {
5151
// eslint-disable-next-line
52-
const middlewares = require('./middlewares').default(runtimeSettings, { dev, dist });
52+
const middlewares = require('./defaultKoaMiddlewares').default(runtimeSettings, { dev, dist });
5353
middlewares.forEach((middleware) => server.use(middleware));
5454
}
5555

packages/roc-package-web-app/src/app/middlewares.js renamed to packages/roc-package-web-app/src/app/defaultKoaMiddlewares.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import koaErrors from 'koa-errors';
2-
import helmet from 'koa-helmet';
1+
import koaError from 'koa-error';
2+
import koaHelmet from 'koa-helmet';
33
import koaEtag from 'koa-etag';
4-
import koaCompressor from 'koa-compressor';
4+
import koaCompress from 'koa-compress';
55
import koaFavicon from 'koa-favicon';
6-
import koaAccesslog from 'koa-accesslog';
76
import koaLogger from 'koa-logger';
7+
import accesslog from './middlewares/accesslog'
88

99
/**
1010
* Returns the middlewares to be used.
@@ -16,17 +16,17 @@ export default function middlewares(config, { dev, dist }) {
1616
const middlewaresList = [];
1717

1818
if (dev) {
19-
middlewaresList.push(koaErrors());
19+
middlewaresList.push(koaError());
2020
}
2121

2222
// Security headers
23-
middlewaresList.push(helmet());
23+
middlewaresList.push(koaHelmet());
2424

2525
middlewaresList.push(koaEtag());
2626

2727
// We only enable gzip in dist
2828
if (dist) {
29-
middlewaresList.push(koaCompressor());
29+
middlewaresList.push(koaCompress());
3030
}
3131

3232
const favicon = config.favicon;
@@ -35,7 +35,7 @@ export default function middlewares(config, { dev, dist }) {
3535
}
3636

3737
if (dist) {
38-
middlewaresList.push(koaAccesslog());
38+
middlewaresList.push(accesslog());
3939
} else {
4040
middlewaresList.push(koaLogger());
4141
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import getKoaMiddlewares from './defaultKoaMiddlewares';
2+
import koaErrorMock from 'koa-error';
3+
import koaHelmetMock from 'koa-helmet';
4+
import koaEtagMock from 'koa-etag'
5+
import koaLoggerMock from 'koa-logger';
6+
import koaCompressMock from 'koa-compress';
7+
import koaFaviconMock from 'koa-favicon';
8+
import accessLogMock from './middlewares/accesslog';
9+
10+
jest.mock('koa-error', () => jest.fn(() => function koaError(){}));
11+
jest.mock('koa-helmet', () => jest.fn(() => function koaHelmet(){}));
12+
jest.mock('koa-etag', () => jest.fn(() => function koaEtag(){}));
13+
jest.mock('koa-logger', () => jest.fn(() => function koaLogger(){}));
14+
jest.mock('koa-compress', () => jest.fn(() => function koaCompress(){}));
15+
jest.mock('koa-favicon', () => jest.fn(() => function koaFavicon(){}));
16+
jest.mock('./middlewares/accesslog', () => jest.fn(() => function accessLog(){}));
17+
18+
19+
describe('defaultKoaMiddleware', () => {
20+
afterEach(() => {
21+
koaErrorMock.mockClear();
22+
koaHelmetMock.mockClear();
23+
koaEtagMock.mockClear();
24+
koaLoggerMock.mockClear();
25+
koaCompressMock.mockClear();
26+
});
27+
28+
it('should return default array of middlewares even when all options are falsy', () => {
29+
const middlewares = getKoaMiddlewares({}, {dev: false, dist: false});
30+
31+
expect(middlewares.length).toEqual(3);
32+
expect(middlewares.map(f => f.name)).toEqual(['koaHelmet', 'koaEtag', 'koaLogger']);
33+
expect(koaHelmetMock).toHaveBeenCalled();
34+
expect(koaEtagMock).toHaveBeenCalled();
35+
expect(koaLoggerMock).toHaveBeenCalled();
36+
});
37+
38+
it('should include koa-error when dev is set to true', () => {
39+
const middlewares = getKoaMiddlewares({}, {dev: true, dist: false});
40+
41+
expect(middlewares.length).toEqual(4);
42+
expect(middlewares.map(f => f.name)).toEqual(['koaError', 'koaHelmet', 'koaEtag', 'koaLogger']);
43+
expect(koaErrorMock).toHaveBeenCalled();
44+
});
45+
46+
it('should include koa-compress and accessLog instead of koa-logger when dist is set to true', () => {
47+
const middlewares = getKoaMiddlewares({}, {dev: false, dist: true});
48+
49+
expect(middlewares.length).toEqual(4);
50+
expect(middlewares.map(f => f.name)).toEqual(['koaHelmet', 'koaEtag', 'koaCompress', 'accessLog']);
51+
expect(koaCompressMock).toHaveBeenCalled();
52+
expect(accessLogMock).toHaveBeenCalled();
53+
});
54+
55+
it('should include koa-favicon when favicon is specified in config', () => {
56+
const config = {favicon: './pathToFavicon.ico'};
57+
const middlewares = getKoaMiddlewares(config, {dev: false, dist: false});
58+
59+
expect(middlewares.length).toEqual(4);
60+
expect(middlewares.map(f => f.name)).toEqual(['koaHelmet', 'koaEtag', 'koaFavicon', 'koaLogger']);
61+
expect(koaFaviconMock).toHaveBeenCalledWith(config.favicon);
62+
})
63+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import moment from 'moment';
2+
import util from 'util';
3+
4+
/*
5+
Implementation based on koa-accesslog
6+
*/
7+
export default function accesslog(stream = process.stdout) {
8+
return async function(ctx, next) {
9+
await next();
10+
11+
const format = '%s - - [%s] "%s %s HTTP/1.X" %d %s\n';
12+
const length = ctx.length ? ctx.length.toString() : '-';
13+
const date = moment().format('D/MMM/YYYY:HH:mm:ss ZZ');
14+
15+
stream.write(util.format(format, ctx.ip, date, ctx.method, ctx.path, ctx.status, length));
16+
}
17+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import createAccessLogMiddleware from './accesslog';
2+
import { format as formatMock } from 'util';
3+
4+
jest.mock('util', () => ({
5+
format: jest.fn(),
6+
}));
7+
8+
describe('middlewares/accesslog', () => {
9+
const next = async () => {};
10+
11+
afterEach(() => {
12+
formatMock.mockClear();
13+
});
14+
15+
it('should call write method of passed stream', async () => {
16+
const streamWriteMock = jest.fn();
17+
const streamMock = {
18+
write: streamWriteMock
19+
};
20+
const accessLogMiddleware = createAccessLogMiddleware(streamMock);
21+
await accessLogMiddleware({}, next);
22+
23+
expect(streamWriteMock).toHaveBeenCalled();
24+
});
25+
26+
it('should call util.format with certain ctx properties', async () => {
27+
const streamWriteMock = jest.fn();
28+
const streamMock = {
29+
write: streamWriteMock
30+
};
31+
const accessLogMiddleware = createAccessLogMiddleware(streamMock);
32+
const ctx = {
33+
length: 42,
34+
ip: '127.0.0.1',
35+
method: 'GET',
36+
path: '/app',
37+
status: 200,
38+
};
39+
await accessLogMiddleware(ctx, next);
40+
41+
expect(formatMock).toHaveBeenCalledWith(
42+
'%s - - [%s] \"%s %s HTTP/1.X\" %d %s\n',
43+
ctx.ip,
44+
expect.anything(),
45+
ctx.method,
46+
ctx.path,
47+
ctx.status,
48+
ctx.length.toString(),
49+
);
50+
});
51+
});

packages/roc-package-web-app/src/app/basepathSupport.js renamed to packages/roc-package-web-app/src/app/middlewares/basepathSupport.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ export default function basepathSupport(basepath) {
2626
return newPath;
2727
}
2828

29-
return function* (next) {
29+
return async function(ctx, next) {
3030
// Do nothing if the basepath is /
3131
if (basepath === '/') {
32-
return yield next;
32+
return await next();
3333
}
3434

35-
const newPath = matcher(this.path);
35+
const newPath = matcher(ctx.path);
3636
if (newPath) {
37-
this.path = newPath;
38-
return yield next;
37+
ctx.path = newPath;
38+
return await next();
3939
}
4040

4141
// If the path does not match Koa will render a default 404 Not Found page.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import createBasepathSupportMiddleware from './basepathSupport';
2+
3+
describe('middlewares/basepathSupport', () => {
4+
const next = async () => {};
5+
6+
it('should throw an error when basepath does not start with "/"', () => {
7+
const veryWrongBasepath = 'veryWrongBasepath';
8+
expect(() => {
9+
createBasepathSupportMiddleware(veryWrongBasepath);
10+
}).toThrowError(`The basepath must start with "/", was ${veryWrongBasepath}`);
11+
});
12+
13+
it('should set new ctx.path with eased basepath', async () => {
14+
const basepath = '/basepath';
15+
const ctx = { path: '/basepath/application' };
16+
const basepathMiddleware = createBasepathSupportMiddleware(basepath);
17+
await basepathMiddleware(ctx, next);
18+
19+
expect(ctx.path).toEqual('/application');
20+
});
21+
22+
it('should set new ctx.path to "/" if its the same as basepath', async () => {
23+
const basepath = '/basepath';
24+
const ctx = { path: '/basepath' };
25+
const basepathMiddleware = createBasepathSupportMiddleware(basepath);
26+
await basepathMiddleware(ctx, next);
27+
28+
expect(ctx.path).toEqual('/');
29+
});
30+
31+
it('should return an undefined when basepath is different than "/" and no new path is set', async () => {
32+
const basepath = '/';
33+
const ctx = { path: '/' };
34+
const basepathMiddleware = createBasepathSupportMiddleware(basepath);
35+
const result = await basepathMiddleware(ctx, next);
36+
37+
expect(result).toEqual(undefined);
38+
});
39+
});

0 commit comments

Comments
 (0)