Skip to content

Instantly share code, notes, and snippets.

@JohnyDays
Last active February 25, 2016 01:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohnyDays/fdbab1f031f0218aee8d to your computer and use it in GitHub Desktop.
Save JohnyDays/fdbab1f031f0218aee8d to your computer and use it in GitHub Desktop.
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const querystring = require('querystring');
const url = require('url');
/**
* Attaches a WebSocket based connection to the Packager to expose
* Hot Module Replacement updates to the simulator.
*/
function attachHMRServer({httpServer, path, packagerServer}) {
let clients = [];
let platformDepencyCache = {
android: {
dependenciesCache: [],
dependenciesModulesCache: {},
shallowDependencies: {},
},
iOS: {
dependenciesCache: [],
dependenciesModulesCache: {},
shallowDependencies: {},
},
}
function sendToClients (message) {
clients.forEach(client => client.ws.send(JSON.stringify(message)));
}
function disconnect (disconnectedClient) {
clients = clients.filter(client => client != disconnectedClient);
//Only clear change listener if there are no more listening clients
if (clients.length === 0) {
packagerServer.setHMRFileChangeListener(null);
}
}
function connectedPlatformClients () {
return {
android: clients.filter(client => client.platform === "android"),
iOS: clients.filter(client => client.platform === "iOS"),
}
}
function forEachPlatform (callback) {
const platformClients = connectedPlatformClients();
if (platformClients.android.length > 0) {
callback("android", platformClients.android);
}
if (platformClients.iOS.length > 0) {
callback("iOS", platformClients.iOS);
}
}
// Runs whenever a file changes
function onHMRChange (filename, stat) {
if (clients.length === 0) {
return;
}
sendToClients({type: 'update-start'});
stat.then(() => {
return packagerServer.getShallowDependencies(filename)
.then(deps => {
if (clients.length === 0) {
return [];
}
forEachPlatform((platform, clients) => {
// if the file dependencies have change we need to invalidate the
// dependencies caches because the list of files we need to send
// to the client may have changed
const oldDependencies = platformDepencyCache[platform].shallowDependencies[filename];
if (arrayEquals(deps, oldDependencies)) {
// Need to create a resolution response to pass to the bundler
// to process requires after transform. By providing a
// specific response we can compute a non recursive one which
// is the least we need and improve performance.
return packagerServer.getDependencies({
platform: platform,
dev: true,
entryFile: filename,
recursive: true,
}).then(response => {
const module = packagerServer.getModuleForPath(filename);
return response.copy({dependencies: [module]});
});
}
// if there're new dependencies compare the full list of
// dependencies we used to have with the one we now have
return getDependencies(platform, client.bundleEntry)
.then(({
dependenciesCache,
dependenciesModulesCache,
shallowDependencies,
resolutionResponse,
}) => {
if (clients.length === 0) {
return {};
}
// build list of modules for which we'll send HMR updates
const modulesToUpdate = [packagerServer.getModuleForPath(filename)];
Object.keys(dependenciesModulesCache).forEach(module => {
if (!client.dependenciesModulesCache[module]) {
modulesToUpdate.push(dependenciesModulesCache[module]);
}
});
// invalidate caches
client.dependenciesCache = dependenciesCache;
client.dependenciesModulesCache = dependenciesModulesCache;
client.shallowDependencies = shallowDependencies;
return resolutionResponse.copy({
dependencies: modulesToUpdate
});
});
})
})
.then((resolutionResponse) => {
if (clients.length === 0) {
return;
}
forEachPlatform((platform, clients) => {
return Promise.all(clients.forEach(client => {
// make sure the file was modified is part of the bundle
if (!client.shallowDependencies[filename]) {
return;
}
return packagerServer.buildBundleForHMR({
entryFile: client.bundleEntry,
platform: client.platform,
resolutionResponse,
});
})
})
})
.then(bundle => {
if ((client.length === 0) || !bundle || bundle.isEmpty()) {
return;
}
return {
type: 'update',
body: {
modules: bundle.getModulesCode(),
sourceURLs: bundle.getSourceURLs(),
sourceMappingURLs: bundle.getSourceMappingURLs(),
},
};
})
})
.catch(error => {
// send errors to the client instead of killing packager server
let body;
if (error.type === 'TransformError' ||
error.type === 'NotFoundError' ||
error.type === 'UnableToResolveError') {
body = {
type: error.type,
description: error.description,
filename: error.filename,
lineNumber: error.lineNumber,
};
} else {
console.error(error.stack || error);
body = {
type: 'InternalError',
description: 'react-packager has encountered an internal error, ' +
'please check your terminal error output for more details',
};
}
return {type: 'error', body};
})
.then(update => {
if ((clients.length === 0) || !update) {
return;
}
sendToClients(update);
});
},
() => {
// do nothing, file was removed
},
).finally(() => {
sendToClients({type: 'update-done'});
});
}
// Returns a promise with the full list of dependencies and the shallow
// dependencies each file on the dependency list has for the give platform
// and entry file.
function getDependencies(platform, bundleEntry) {
return packagerServer.getDependencies({
platform: platform,
dev: true,
entryFile: bundleEntry,
}).then(response => {
// for each dependency builds the object:
// `{path: '/a/b/c.js', deps: ['modA', 'modB', ...]}`
return Promise.all(Object.values(response.dependencies).map(dep => {
return dep.getName().then(depName => {
if (dep.isAsset() || dep.isAsset_DEPRECATED() || dep.isJSON()) {
return Promise.resolve({path: dep.path, deps: []});
}
return packagerServer.getShallowDependencies(dep.path)
.then(deps => {
return {
path: dep.path,
name: depName,
deps,
};
});
});
}))
.then(deps => {
// list with all the dependencies' filenames the bundle entry has
const dependenciesCache = response.dependencies.map(dep => dep.path);
// map from module name to path
const moduleToFilenameCache = Object.create(null);
deps.forEach(dep => moduleToFilenameCache[dep.name] = dep.path);
// map that indicates the shallow dependency each file included on the
// bundle has
const shallowDependencies = Object.create(null);
deps.forEach(dep => shallowDependencies[dep.path] = dep.deps);
// map from module name to the modules' dependencies the bundle entry
// has
const dependenciesModulesCache = Object.create(null);
return Promise.all(response.dependencies.map(dep => {
return dep.getName().then(depName => {
dependenciesModulesCache[depName] = dep;
});
})).then(() => {
return {
dependenciesCache,
dependenciesModulesCache,
shallowDependencies,
resolutionResponse: response,
};
});
});
});
}
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({
server: httpServer,
path: path,
});
console.log('[Hot Module Replacement] Server listening on', path);
wss.on('connection', ws => {
console.log('[Hot Module Replacement] Client connected');
const params = querystring.parse(url.parse(ws.upgradeReq.url).query);
getDependencies(params.platform, params.bundleEntry)
.then(({
dependenciesCache,
dependenciesModulesCache,
shallowDependencies,
}) => {
client = {
ws,
platform: params.platform,
bundleEntry: params.bundleEntry,
};
//Set the platform dependency cache when a new client connects
platformDepencyCache[params.platform] = {
dependenciesCache,
dependenciesModulesCache,
shallowDependencies,
};
packagerServer.setHMRFileChangeListener(onHMRChange);
client.ws.on('error', e => {
console.error('[Hot Module Replacement] Unexpected error', e);
disconnect(client);
});
client.ws.on('close', () => disconnect(client));
})
.done();
});
}
function arrayEquals(arrayA, arrayB) {
arrayA = arrayA || [];
arrayB = arrayB || [];
return (
arrayA.length === arrayB.length &&
arrayA.every((element, index) => {
return element === arrayB[index];
})
);
}
module.exports = attachHMRServer;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment