Skip to content

Commit 7536d23

Browse files
committed
chore: implementing sockets
1 parent 3e65e81 commit 7536d23

14 files changed

+363
-62
lines changed

.eslintrc

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
{
2-
"extends": "shellscape"
2+
"extends": "shellscape",
3+
"globals": {
4+
"WebSocket": true,
5+
"window": true
6+
}
37
}

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ node_modules
55

66
output.js
77
output.js.map
8+
*.hot-update.*

lib/builtins.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
/*
2+
Copyright © 2018 Andrew Powell
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
8+
The above copyright notice and this permission notice shall be
9+
included in all copies or substantial portions of this Source Code Form.
10+
*/
111
const convert = require('koa-connect');
212
const historyApiFallback = require('connect-history-api-fallback');
313
const koaCompress = require('koa-compress');
414
const koaStatic = require('koa-static');
515
const onetime = require('onetime');
616

17+
const { middleware: wsMiddleware } = require('./ws');
18+
719
// TODO: add proxy
820

921
const getBuiltins = (app, options) => {
@@ -27,10 +39,13 @@ const getBuiltins = (app, options) => {
2739
}
2840
};
2941

42+
const websocket = () => app.use(wsMiddleware);
43+
3044
return {
3145
compress: onetime(compress),
3246
historyFallback: onetime(historyFallback),
33-
static: onetime(statik)
47+
static: onetime(statik),
48+
websocket: onetime(websocket)
3449
};
3550
};
3651

lib/client.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
Copyright © 2018 Andrew Powell
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
8+
The above copyright notice and this permission notice shall be
9+
included in all copies or substantial portions of this Source Code Form.
10+
*/
11+
(() => {
12+
// eslint-disable-next-line no-undef, no-unused-vars
13+
const options = ʎɐɹɔosǝʌɹǝs;
14+
const { info, warn } = console;
15+
const socket = new WebSocket('ws://[::]:55555/wps');
16+
17+
// prevents ECONNRESET errors on the server
18+
window.addEventListener('beforeunload', () => socket.close());
19+
20+
socket.onmessage = (message) => {
21+
const { action, data } = JSON.parse(message.data);
22+
switch (action) {
23+
case 'connected':
24+
info('⬡ wps: WebSocket connected');
25+
break;
26+
case 'invalid':
27+
info(data);
28+
break;
29+
default:
30+
}
31+
};
32+
33+
socket.onclose = () => warn(`⬡ wps: The client WebSocket was closed. Please refresh the page.`);
34+
})();

lib/index.js

+40-42
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,46 @@
99
included in all copies or substantial portions of this Source Code Form.
1010
*/
1111
const EventEmitter = require('events');
12-
const url = require('url');
1312

1413
const Koa = require('koa');
15-
const open = require('opn');
14+
const { DefinePlugin, HotModuleReplacementPlugin } = require('webpack');
1615

17-
const { getBuiltins } = require('./builtins');
16+
const { getLogger } = require('./log');
17+
const { start } = require('./server');
1818

1919
const defaults = {
2020
compress: null,
2121
historyFallback: false,
22+
hmr: true,
2223
host: () => null,
24+
log: { level: 'info' },
2325
middleware: () => {},
24-
open: true,
26+
open: false,
2527
port: 55555,
2628
static: null
2729
};
2830

29-
// TODO: write a proper logger
30-
const { warn } = console;
31+
const key = 'webpack-plugin-serve';
32+
const newline = () => console.log(); // eslint-disable-line no-console
3133

32-
let instantiated = false;
34+
let instance = null;
3335

36+
// TODO: test this on a multicompiler setup
3437
// TODO: https by default
3538
// TODO: wire up websockets
3639
// TODO: create client script that connects to the websockets
3740
class WebpackPluginServe extends EventEmitter {
3841
constructor(opts) {
39-
if (instantiated) {
40-
// TODO: expand on this error message
41-
warn('webpack-plugin-serve is only meant to be used once per config');
42-
}
42+
super();
4343

44-
instantiated = true;
44+
if (instance) {
45+
instance.log.error(
46+
'Duplicate instances created. Only the first instance of this plugin will be active.'
47+
);
48+
return;
49+
}
4550

46-
super();
51+
instance = this;
4752

4853
const options = Object.assign({}, defaults, opts);
4954

@@ -63,46 +68,39 @@ class WebpackPluginServe extends EventEmitter {
6368
options.port = () => usePort;
6469
}
6570

71+
this.app = new Koa();
72+
this.log = getLogger(options.log || {});
6673
this.options = options;
6774
}
6875

6976
apply(compiler) {
70-
if (!this.options.static) {
71-
this.options.static = [compiler.context];
72-
}
77+
this.compiler = compiler;
7378

74-
compiler.hooks.done.tap('WebpackPluginServe', this.start.bind(this));
75-
}
76-
77-
async start() {
78-
const app = new Koa();
79-
const { host, middleware, port } = this.options;
80-
const builtins = getBuiltins(app, this.options);
81-
const useHost = await host(); // eslint-disable-line
82-
const usePort = await port();
83-
84-
// allow users to add and manipulate middleware in the config
85-
await middleware(app, builtins);
86-
87-
// call each of the builtin middleware. methods are once'd so this has no ill-effects.
88-
for (const fn of Object.values(builtins)) {
89-
fn();
79+
if (instance !== this) {
80+
return;
9081
}
9182

92-
// TODO: use app.callback and setup our own server instance
93-
const server = app.listen(usePort);
94-
// TODO: circle back to this when https is enabled
95-
const protocol = 'http';
96-
const address = server.address();
83+
const { done, watchRun } = compiler.hooks;
9784

98-
address.hostname = address.address;
99-
const uri = `${protocol}://${url.format(address)}`;
85+
if (!this.options.static) {
86+
this.options.static = [compiler.context];
87+
}
10088

101-
warn('\nListening on:', uri);
89+
done.tap(key, start.bind(this));
90+
watchRun.tap(key, () => {
91+
if (this.listening) {
92+
newline();
93+
}
94+
});
10295

103-
if (this.options.open) {
104-
open(uri, this.options.open === true ? {} : this.options.open);
96+
if (this.options.hmr) {
97+
const hmrPlugin = new HotModuleReplacementPlugin();
98+
hmrPlugin.apply(compiler);
10599
}
100+
101+
const defineData = { ʎɐɹɔosǝʌɹǝs: JSON.stringify(this.options) };
102+
const definePlugin = new DefinePlugin(defineData);
103+
definePlugin.apply(compiler);
106104
}
107105
}
108106

lib/log.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright © 2018 Andrew Powell
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
8+
The above copyright notice and this permission notice shall be
9+
included in all copies or substantial portions of this Source Code Form.
10+
*/
11+
const chalk = require('chalk');
12+
const loglevel = require('loglevelnext');
13+
14+
const symbols = { ok: '⬡', whoops: '⬢' };
15+
const colors = {
16+
trace: 'cyan',
17+
debug: 'magenta',
18+
info: 'blue',
19+
warn: 'yellow',
20+
error: 'red'
21+
};
22+
23+
const getLogger = (options) => {
24+
const prefix = {
25+
level: ({ level }) => {
26+
const color = colors[level];
27+
const symbol = ['error', 'warn'].includes(level) ? symbols.whoops : symbols.ok;
28+
return chalk[color](`${symbol} wps: `);
29+
},
30+
template: '{{level}}'
31+
};
32+
33+
if (options.timestamp) {
34+
prefix.template = `[{{time}}] ${prefix.template}`;
35+
}
36+
37+
/* eslint-disable no-param-reassign */
38+
options.prefix = prefix;
39+
options.name = 'webpack-plugin-serve';
40+
41+
const log = loglevel.create(options);
42+
43+
return log;
44+
};
45+
46+
module.exports = { getLogger };

lib/routes.js

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright © 2018 Andrew Powell
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
8+
The above copyright notice and this permission notice shall be
9+
included in all copies or substantial portions of this Source Code Form.
10+
*/
11+
const router = require('koa-route');
12+
13+
const key = 'webpack-plugin-serve';
14+
15+
const prep = (data) => JSON.stringify(data);
16+
17+
const setupRoutes = function setupRoutes() {
18+
const { app, compiler } = this;
19+
const { invalid } = compiler.hooks;
20+
const connect = async (ctx) => {
21+
if (ctx.ws) {
22+
const socket = await ctx.ws();
23+
24+
socket.invalid = (filePath = '<unknown>') => {
25+
if (socket.readyState === 3) {
26+
return;
27+
}
28+
29+
const context = compiler.context || compiler.options.context || process.cwd();
30+
const fileName = filePath.replace(context, '');
31+
32+
socket.send(prep({ action: 'invalid', data: { fileName } }));
33+
};
34+
35+
// we do this because webpack caches and optimizes the hooks, so there's no way to detach a
36+
// listener/hook.
37+
invalid.tap(key, (filePath) => this.emit('invalid', filePath));
38+
39+
this.on('invalid', socket.invalid);
40+
41+
socket.on('close', () => {
42+
this.removeListener('invalid', socket.invalid);
43+
});
44+
45+
socket.send(prep({ action: 'connected' }));
46+
}
47+
};
48+
49+
app.use(router.get('/wps', connect));
50+
};
51+
52+
module.exports = { setupRoutes };

lib/server.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
Copyright © 2018 Andrew Powell
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public
5+
License, v. 2.0. If a copy of the MPL was not distributed with this
6+
file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
8+
The above copyright notice and this permission notice shall be
9+
included in all copies or substantial portions of this Source Code Form.
10+
*/
11+
const url = require('url');
12+
13+
const open = require('opn');
14+
15+
const { getBuiltins } = require('./builtins');
16+
const { setupRoutes } = require('./routes');
17+
18+
const newline = () => console.log(); // eslint-disable-line no-console
19+
20+
const start = async function start() {
21+
if (this.listening) {
22+
return;
23+
}
24+
25+
const { app } = this;
26+
const { host, middleware, port } = this.options;
27+
const builtins = getBuiltins(app, this.options);
28+
const useHost = await host(); // eslint-disable-line
29+
const usePort = await port();
30+
31+
// allow users to add and manipulate middleware in the config
32+
await middleware(app, builtins);
33+
34+
// call each of the builtin middleware. methods are once'd so this has no ill-effects.
35+
for (const fn of Object.values(builtins)) {
36+
fn();
37+
}
38+
39+
setupRoutes.bind(this)();
40+
41+
// TODO: use app.callback and setup our own server instance
42+
const server = app.listen(usePort);
43+
this.listening = true;
44+
45+
// TODO: circle back to this when https is enabled
46+
const protocol = 'http';
47+
const address = server.address();
48+
49+
address.hostname = address.address;
50+
const uri = `${protocol}://${url.format(address)}`;
51+
52+
newline();
53+
this.log.info('Server Listening on:', uri);
54+
55+
if (this.options.open) {
56+
open(uri, this.options.open === true ? {} : this.options.open);
57+
}
58+
};
59+
60+
module.exports = { start };

0 commit comments

Comments
 (0)