Add webpack building (#907)

* friggin webpack tho

* probably important

* add all the stuff to the package.json

* add minify builds and a nice package.json script to run it all

* clean up

* use uglify harmony branch so we can actually run minify builds that work

* update build system

* make test better

* clean up

* fix issues with compression

* ‮

* c++ requirements in a node lib? whaaaaat?

* fix travis yml?

* put railings on voice connections

* 🖕🏻

* aaaaaa

* handle arraybuffers in the unlikely event one is sent

* support arraybuffers in resolvebuffer

* this needs to be fixed at some point

* this was fixed

* disable filename versioning if env VERSIONED is set to false

* Update ClientDataResolver.js

* Update ClientVoiceManager.js

* Update WebSocketManager.js

* Update ConvertArrayBuffer.js

* Update webpack.html

* enable compression for browser and fix ws error handler

* Update WebSocketManager.js

* everything will be okay gawdl3y

* compression is slower in browser, so rip the last three hours of my life

* Update Constants.js

* Update .gitignore
This commit is contained in:
Gus Caplan
2016-11-20 18:38:16 -06:00
committed by Schuyler Cebulskie
parent b3e795d0b0
commit 2440a4a2c8
14 changed files with 162 additions and 38 deletions

2
.gitignore vendored
View File

@@ -8,6 +8,7 @@ logs/
# Authentication # Authentication
test/auth.json test/auth.json
test/auth.js
docs/deploy/deploy_key docs/deploy/deploy_key
docs/deploy/deploy_key.pub docs/deploy/deploy_key.pub
@@ -15,3 +16,4 @@ docs/deploy/deploy_key.pub
.tmp/ .tmp/
.vscode/ .vscode/
docs/docs.json docs/docs.json
webpack/

View File

@@ -10,4 +10,10 @@ env:
global: global:
- ENCRYPTION_LABEL: "af862fa96d3e" - ENCRYPTION_LABEL: "af862fa96d3e"
- COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com" - COMMIT_AUTHOR_EMAIL: "amishshah.2k@gmail.com"
- CXX=g++-4.9
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.9

View File

@@ -7,7 +7,8 @@
"test": "eslint src && node docs/generator test", "test": "eslint src && node docs/generator test",
"docs": "node docs/generator", "docs": "node docs/generator",
"test-docs": "node docs/generator test", "test-docs": "node docs/generator test",
"lint": "eslint src" "lint": "eslint src",
"web-dist": "npm install && ./node_modules/parallel-webpack/bin/run.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -29,16 +30,23 @@
"homepage": "https://github.com/hydrabolt/discord.js#readme", "homepage": "https://github.com/hydrabolt/discord.js#readme",
"dependencies": { "dependencies": {
"superagent": "^2.3.0", "superagent": "^2.3.0",
"tweetnacl": "^0.14.0", "tweetnacl": "^0.14.3",
"ws": "^1.1.0" "ws": "^1.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"node-opus": "^0.2.0", "node-opus": "^0.2.0",
"opusscript": "^0.0.1" "opusscript": "^0.0.1"
}, },
"devDependencies": { "devDependencies": {
"bufferutil": "^1.2.1",
"eslint": "^3.10.0", "eslint": "^3.10.0",
"jsdoc-to-markdown": "^2.0.0" "jsdoc-to-markdown": "^2.0.0",
"json-loader": "^0.5.4",
"parallel-webpack": "^1.5.0",
"uglify-js": "github:mishoo/UglifyJS2#harmony",
"utf-8-validate": "^1.2.1",
"webpack": "^1.13.3",
"zlibjs": "github:imaya/zlib.js"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"

View File

@@ -23,6 +23,12 @@ class Client extends EventEmitter {
constructor(options = {}) { constructor(options = {}) {
super(); super();
/**
* Whether the client is in a browser environment
* @type {boolean}
*/
this.browser = typeof window !== 'undefined';
// Obtain shard details from environment // Obtain shard details from environment
if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID); if (!options.shardId && 'SHARD_ID' in process.env) options.shardId = Number(process.env.SHARD_ID);
if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT); if (!options.shardCount && 'SHARD_COUNT' in process.env) options.shardCount = Number(process.env.SHARD_COUNT);

View File

@@ -3,13 +3,14 @@ const fs = require('fs');
const request = require('superagent'); const request = require('superagent');
const Constants = require('../util/Constants'); const Constants = require('../util/Constants');
const User = require(`../structures/User`); const convertArrayBuffer = require('../util/ConvertArrayBuffer');
const Message = require(`../structures/Message`); const User = require('../structures/User');
const Guild = require(`../structures/Guild`); const Message = require('../structures/Message');
const Channel = require(`../structures/Channel`); const Guild = require('../structures/Guild');
const GuildMember = require(`../structures/GuildMember`); const Channel = require('../structures/Channel');
const Emoji = require(`../structures/Emoji`); const GuildMember = require('../structures/GuildMember');
const ReactionEmoji = require(`../structures/ReactionEmoji`); const Emoji = require('../structures/Emoji');
const ReactionEmoji = require('../structures/ReactionEmoji');
/** /**
* The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g. * The DataResolver identifies different objects and tries to resolve a specific piece of information from them, e.g.
@@ -240,14 +241,16 @@ class ClientDataResolver {
*/ */
resolveBuffer(resource) { resolveBuffer(resource) {
if (resource instanceof Buffer) return Promise.resolve(resource); if (resource instanceof Buffer) return Promise.resolve(resource);
if (this.client.browser && resource instanceof ArrayBuffer) return Promise.resolve(convertArrayBuffer(resource));
if (typeof resource === 'string') { if (typeof resource === 'string') {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (/^https?:\/\//.test(resource)) { if (/^https?:\/\//.test(resource)) {
request.get(resource) const req = request.get(resource).set('Content-Type', 'blob');
.set('Content-Type', 'blob') if (this.client.browser) req.responseType('arraybuffer');
.end((err, res) => { req.end((err, res) => {
if (err) return reject(err); if (err) return reject(err);
if (this.client.browser) return resolve(convertArrayBuffer(res.xhr.response));
if (!(res.body instanceof Buffer)) return reject(new TypeError('Body is not a Buffer')); if (!(res.body instanceof Buffer)) return reject(new TypeError('Body is not a Buffer'));
return resolve(res.body); return resolve(res.body);
}); });

View File

@@ -3,14 +3,13 @@ const Collection = require('../../util/Collection');
const splitMessage = require('../../util/SplitMessage'); const splitMessage = require('../../util/SplitMessage');
const parseEmoji = require('../../util/ParseEmoji'); const parseEmoji = require('../../util/ParseEmoji');
const requireStructure = name => require(`../../structures/${name}`); const User = require('../../structures/User');
const User = requireStructure('User'); const GuildMember = require('../../structures/GuildMember');
const GuildMember = requireStructure('GuildMember'); const Role = require('../../structures/Role');
const Role = requireStructure('Role'); const Invite = require('../../structures/Invite');
const Invite = requireStructure('Invite'); const Webhook = require('../../structures/Webhook');
const Webhook = requireStructure('Webhook'); const UserProfile = require('../../structures/UserProfile');
const UserProfile = requireStructure('UserProfile'); const ClientOAuth2Application = require('../../structures/ClientOAuth2Application');
const ClientOAuth2Application = requireStructure('ClientOAuth2Application');
class RESTMethods { class RESTMethods {
constructor(restManager) { constructor(restManager) {

View File

@@ -78,6 +78,7 @@ class ClientVoiceManager {
*/ */
joinChannel(channel) { joinChannel(channel) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.client.browser) throw new Error('Voice connections are not available in browsers.');
if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.'); if (this.pending.get(channel.guild.id)) throw new Error('Already connecting to this guild\'s voice server.');
if (!channel.joinable) throw new Error('You do not have permission to join this voice channel.'); if (!channel.joinable) throw new Error('You do not have permission to join this voice channel.');

View File

@@ -1,8 +1,10 @@
const WebSocket = require('ws'); const browser = typeof window !== 'undefined';
const WebSocket = browser ? window.WebSocket : require('ws'); // eslint-disable-line no-undef
const EventEmitter = require('events').EventEmitter; const EventEmitter = require('events').EventEmitter;
const Constants = require('../../util/Constants'); const Constants = require('../../util/Constants');
const zlib = require('zlib'); const inflate = browser ? require('zlibjs').inflateSync : require('zlib').inflateSync;
const PacketManager = require('./packets/WebSocketPacketManager'); const PacketManager = require('./packets/WebSocketPacketManager');
const convertArrayBuffer = require('../../util/ConvertArrayBuffer');
/** /**
* The WebSocket Manager of the Client * The WebSocket Manager of the Client
@@ -78,6 +80,7 @@ class WebSocketManager extends EventEmitter {
this.normalReady = false; this.normalReady = false;
if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING; if (this.status !== Constants.Status.RECONNECTING) this.status = Constants.Status.CONNECTING;
this.ws = new WebSocket(gateway); this.ws = new WebSocket(gateway);
if (browser) this.ws.binaryType = 'arraybuffer';
this.ws.onopen = () => this.eventOpen(); this.ws.onopen = () => this.eventOpen();
this.ws.onclose = (d) => this.eventClose(d); this.ws.onclose = (d) => this.eventClose(d);
this.ws.onmessage = (e) => this.eventMessage(e); this.ws.onmessage = (e) => this.eventMessage(e);
@@ -193,11 +196,11 @@ class WebSocketManager extends EventEmitter {
*/ */
eventClose(event) { eventClose(event) {
this.emit('close', event); this.emit('close', event);
this.client.clearInterval(this.client.manager.heartbeatInterval);
/** /**
* Emitted whenever the client websocket is disconnected * Emitted whenever the client websocket is disconnected
* @event Client#disconnect * @event Client#disconnect
*/ */
clearInterval(this.client.manager.heartbeatInterval);
if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT); if (!this.reconnecting) this.client.emit(Constants.Events.DISCONNECT);
if (event.code === 4004) return; if (event.code === 4004) return;
if (event.code === 4010) return; if (event.code === 4010) return;
@@ -211,10 +214,13 @@ class WebSocketManager extends EventEmitter {
* @returns {boolean} * @returns {boolean}
*/ */
eventMessage(event) { eventMessage(event) {
let packet; let packet = event.data;
try { try {
if (event.binary) event.data = zlib.inflateSync(event.data).toString(); if (typeof packet !== 'string') {
packet = JSON.parse(event.data); if (packet instanceof ArrayBuffer) packet = convertArrayBuffer(packet);
packet = inflate(packet).toString();
}
packet = JSON.parse(packet);
} catch (e) { } catch (e) {
return this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE)); return this.eventError(new Error(Constants.Errors.BAD_WS_MESSAGE));
} }
@@ -236,7 +242,7 @@ class WebSocketManager extends EventEmitter {
* @param {Error} error The encountered error * @param {Error} error The encountered error
*/ */
if (this.client.listenerCount('error') > 0) this.client.emit('error', err); if (this.client.listenerCount('error') > 0) this.client.emit('error', err);
this.tryReconnect(); this.ws.close();
} }
_emitReady(normal = true) { _emitReady(normal = true) {

View File

@@ -1,7 +1,6 @@
const AbstractHandler = require('./AbstractHandler'); const AbstractHandler = require('./AbstractHandler');
const getStructure = name => require(`../../../../structures/${name}`); const ClientUser = require('../../../../structures/ClientUser');
const ClientUser = getStructure('ClientUser');
class ReadyHandler extends AbstractHandler { class ReadyHandler extends AbstractHandler {
handle(packet) { handle(packet) {

View File

@@ -41,3 +41,5 @@ module.exports = {
version: require('../package').version, version: require('../package').version,
}; };
if (typeof window !== 'undefined') window.Discord = module.exports; // eslint-disable-line no-undef

View File

@@ -42,11 +42,12 @@ exports.DefaultOptions = {
* Websocket options. These are left as snake_case to match the API. * Websocket options. These are left as snake_case to match the API.
* @typedef {Object} WebsocketOptions * @typedef {Object} WebsocketOptions
* @property {number} [large_threshold=250] Number of members in a guild to be considered large * @property {number} [large_threshold=250] Number of members in a guild to be considered large
* @property {boolean} [compress=true] Whether to compress data sent on the connection * @property {boolean} [compress=true] Whether to compress data sent on the connection.
* Defaults to `false` for browsers.
*/ */
ws: { ws: {
large_threshold: 250, large_threshold: 250,
compress: true, compress: typeof window === 'undefined',
properties: { properties: {
$os: process ? process.platform : 'discord.js', $os: process ? process.platform : 'discord.js',
$browser: 'discord.js', $browser: 'discord.js',

View File

@@ -0,0 +1,18 @@
function arrayBufferToBuffer(ab) {
const buffer = new Buffer(ab.byteLength);
const view = new Uint8Array(ab);
for (var i = 0; i < buffer.length; ++i) buffer[i] = view[i];
return buffer;
}
function str2ab(str) {
const buffer = new ArrayBuffer(str.length * 2);
const view = new Uint16Array(buffer);
for (var i = 0, strLen = str.length; i < strLen; i++) view[i] = str.charCodeAt(i);
return buffer;
}
module.exports = function convertArrayBuffer(x) {
if (typeof x === 'string') x = str2ab(x);
return arrayBufferToBuffer(x);
};

30
test/webpack.html Normal file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>discord.js Webpack test</title>
<meta charset="utf-8" />
</head>
<body>
<script type="text/javascript" src="../webpack/discord.10.0.1.js"></script>
<script type="text/javascript" src="auth.js"></script>
<script type="text/javascript">
(() => {
const client = window.client = new Discord.Client();
client.on('ready', () => {
console.log('[CLIENT] Ready!');
});
client.on('error', console.error);
client.ws.on('close', (event) => console.log('[CLIENT] Disconnect!', event));
client.on('message', (message) => {
console.log(message.author.username, message.author.id, message.content);
});
client.login(window.token || prompt('token', 'abcdef123456'));
})();
</script>
</body>
</html>

43
webpack.config.js Normal file
View File

@@ -0,0 +1,43 @@
/*
ONLY RUN BUILDS WITH `npm run web-dist`!
DO NOT USE NORMAL WEBPACK! IT WILL NOT WORK!
*/
const webpack = require('webpack');
const createVariants = require('parallel-webpack').createVariants;
const version = require('./package.json').version;
const createConfig = (options) => {
const plugins = [
new webpack.DefinePlugin({ 'global.GENTLY': false }),
];
if (options.minify) plugins.push(new webpack.optimize.UglifyJsPlugin({ minimize: true }));
const filename = `./webpack/discord${process.env.VERSIONED === 'false' ? '' : '.' + version}${options.minify ? '.min' : ''}.js`; // eslint-disable-line
return {
entry: './src/index.js',
output: {
path: __dirname,
filename,
},
module: {
loaders: [
{ test: /\.json$/, loader: 'json-loader' },
{ test: /\.md$/, loader: 'ignore-loader' },
],
},
node: {
fs: 'empty',
dns: 'mock',
tls: 'mock',
child_process: 'empty',
dgram: 'empty',
__dirname: true,
},
plugins,
};
};
module.exports = createVariants({}, { minify: [false, true] }, createConfig);