2015-12-29 00:43:08 +00:00
|
|
|
/**
|
|
|
|
* 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';
|
|
|
|
|
2016-02-26 23:14:47 +00:00
|
|
|
const getInverseDependencies = require('node-haste').getInverseDependencies;
|
2015-12-30 02:24:06 +00:00
|
|
|
const querystring = require('querystring');
|
|
|
|
const url = require('url');
|
|
|
|
|
2016-03-09 19:49:40 +00:00
|
|
|
const blacklist = [
|
|
|
|
'Libraries/Utilities/HMRClient.js',
|
|
|
|
];
|
|
|
|
|
2015-12-29 00:43:08 +00:00
|
|
|
/**
|
|
|
|
* Attaches a WebSocket based connection to the Packager to expose
|
|
|
|
* Hot Module Replacement updates to the simulator.
|
|
|
|
*/
|
|
|
|
function attachHMRServer({httpServer, path, packagerServer}) {
|
2015-12-30 02:24:06 +00:00
|
|
|
let client = null;
|
2015-12-29 00:43:21 +00:00
|
|
|
|
|
|
|
function disconnect() {
|
2015-12-30 02:24:06 +00:00
|
|
|
client = null;
|
2015-12-30 02:24:10 +00:00
|
|
|
packagerServer.setHMRFileChangeListener(null);
|
2015-12-29 00:43:21 +00:00
|
|
|
}
|
|
|
|
|
2016-02-26 23:14:47 +00:00
|
|
|
// For the give platform and entry file, returns a promise with:
|
|
|
|
// - The full list of dependencies.
|
|
|
|
// - The shallow dependencies each file on the dependency list has
|
|
|
|
// - Inverse shallow dependencies map
|
2015-12-30 02:24:08 +00:00
|
|
|
function getDependencies(platform, bundleEntry) {
|
|
|
|
return packagerServer.getDependencies({
|
|
|
|
platform: platform,
|
|
|
|
dev: true,
|
2016-03-08 17:50:14 +00:00
|
|
|
hot: true,
|
2015-12-30 02:24:08 +00:00
|
|
|
entryFile: bundleEntry,
|
|
|
|
}).then(response => {
|
2016-03-14 23:16:22 +00:00
|
|
|
const {getModuleId} = response;
|
|
|
|
|
2015-12-30 02:24:08 +00:00
|
|
|
// for each dependency builds the object:
|
|
|
|
// `{path: '/a/b/c.js', deps: ['modA', 'modB', ...]}`
|
|
|
|
return Promise.all(Object.values(response.dependencies).map(dep => {
|
2016-01-04 21:01:28 +00:00
|
|
|
return dep.getName().then(depName => {
|
|
|
|
if (dep.isAsset() || dep.isAsset_DEPRECATED() || dep.isJSON()) {
|
|
|
|
return Promise.resolve({path: dep.path, deps: []});
|
|
|
|
}
|
2016-04-06 18:36:57 +00:00
|
|
|
return packagerServer.getShallowDependencies({
|
|
|
|
platform: platform,
|
|
|
|
dev: true,
|
|
|
|
hot: true,
|
|
|
|
entryFile: dep.path
|
|
|
|
})
|
2016-01-04 21:01:28 +00:00
|
|
|
.then(deps => {
|
|
|
|
return {
|
|
|
|
path: dep.path,
|
|
|
|
name: depName,
|
|
|
|
deps,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
2015-12-30 02:24:08 +00:00
|
|
|
}))
|
|
|
|
.then(deps => {
|
2016-01-04 21:01:28 +00:00
|
|
|
// list with all the dependencies' filenames the bundle entry has
|
2015-12-30 02:24:08 +00:00
|
|
|
const dependenciesCache = response.dependencies.map(dep => dep.path);
|
|
|
|
|
2016-01-04 21:01:28 +00:00
|
|
|
// map from module name to path
|
|
|
|
const moduleToFilenameCache = Object.create(null);
|
2016-03-12 00:16:05 +00:00
|
|
|
deps.forEach(dep => {
|
|
|
|
moduleToFilenameCache[dep.name] = dep.path;
|
|
|
|
});
|
2016-01-04 21:01:28 +00:00
|
|
|
|
2015-12-30 02:24:08 +00:00
|
|
|
// map that indicates the shallow dependency each file included on the
|
|
|
|
// bundle has
|
2016-01-04 21:01:28 +00:00
|
|
|
const shallowDependencies = Object.create(null);
|
2016-03-12 00:16:05 +00:00
|
|
|
deps.forEach(dep => {
|
|
|
|
shallowDependencies[dep.path] = dep.deps;
|
|
|
|
});
|
2015-12-30 02:24:08 +00:00
|
|
|
|
2016-01-04 21:01:28 +00:00
|
|
|
// map from module name to the modules' dependencies the bundle entry
|
|
|
|
// has
|
|
|
|
const dependenciesModulesCache = Object.create(null);
|
2016-03-14 23:16:22 +00:00
|
|
|
response.dependencies.forEach(dep => {
|
|
|
|
dependenciesModulesCache[getModuleId(dep)] = dep;
|
2016-01-04 21:01:28 +00:00
|
|
|
});
|
2016-03-14 23:16:22 +00:00
|
|
|
|
|
|
|
|
|
|
|
const inverseDependenciesCache = Object.create(null);
|
|
|
|
const inverseDependencies = getInverseDependencies(response);
|
|
|
|
for (const [module, dependents] of inverseDependencies) {
|
|
|
|
inverseDependenciesCache[getModuleId(module)] =
|
|
|
|
Array.from(dependents).map(getModuleId);
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
dependenciesCache,
|
|
|
|
dependenciesModulesCache,
|
|
|
|
shallowDependencies,
|
|
|
|
inverseDependenciesCache,
|
|
|
|
resolutionResponse: response,
|
|
|
|
};
|
2015-12-30 02:24:08 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-12-29 00:43:08 +00:00
|
|
|
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');
|
2015-12-30 02:24:06 +00:00
|
|
|
const params = querystring.parse(url.parse(ws.upgradeReq.url).query);
|
2015-12-29 00:43:08 +00:00
|
|
|
|
2015-12-30 02:24:08 +00:00
|
|
|
getDependencies(params.platform, params.bundleEntry)
|
2016-01-04 21:01:28 +00:00
|
|
|
.then(({
|
|
|
|
dependenciesCache,
|
|
|
|
dependenciesModulesCache,
|
|
|
|
shallowDependencies,
|
2016-02-26 23:14:47 +00:00
|
|
|
inverseDependenciesCache,
|
2016-01-04 21:01:28 +00:00
|
|
|
}) => {
|
2015-12-30 02:24:08 +00:00
|
|
|
client = {
|
|
|
|
ws,
|
|
|
|
platform: params.platform,
|
|
|
|
bundleEntry: params.bundleEntry,
|
|
|
|
dependenciesCache,
|
2016-01-04 21:01:28 +00:00
|
|
|
dependenciesModulesCache,
|
2015-12-30 02:24:08 +00:00
|
|
|
shallowDependencies,
|
2016-02-26 23:14:47 +00:00
|
|
|
inverseDependenciesCache,
|
2015-12-30 02:24:08 +00:00
|
|
|
};
|
|
|
|
|
2016-01-07 16:48:39 +00:00
|
|
|
packagerServer.setHMRFileChangeListener((filename, stat) => {
|
2015-12-30 02:24:10 +00:00
|
|
|
if (!client) {
|
2016-01-29 18:14:43 +00:00
|
|
|
return;
|
2015-12-30 02:24:10 +00:00
|
|
|
}
|
2016-02-26 16:51:28 +00:00
|
|
|
console.log(
|
|
|
|
`[Hot Module Replacement] File change detected (${time()})`
|
|
|
|
);
|
2015-12-30 02:24:10 +00:00
|
|
|
|
2016-03-12 00:16:05 +00:00
|
|
|
const blacklisted = blacklist.find(blacklistedPath =>
|
|
|
|
filename.indexOf(blacklistedPath) !== -1
|
2016-03-09 19:49:40 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
if (blacklisted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-01 20:41:04 +00:00
|
|
|
client.ws.send(JSON.stringify({type: 'update-start'}));
|
2016-01-29 18:14:43 +00:00
|
|
|
stat.then(() => {
|
2016-04-06 18:36:57 +00:00
|
|
|
return packagerServer.getShallowDependencies({
|
|
|
|
entryFile: filename,
|
|
|
|
platform: client.platform,
|
|
|
|
dev: true,
|
|
|
|
hot: true,
|
|
|
|
})
|
2016-01-07 16:48:39 +00:00
|
|
|
.then(deps => {
|
|
|
|
if (!client) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 = client.shallowDependencies[filename];
|
|
|
|
if (arrayEquals(deps, oldDependencies)) {
|
2016-01-29 18:14:37 +00:00
|
|
|
// 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: client.platform,
|
|
|
|
dev: true,
|
2016-03-08 17:50:14 +00:00
|
|
|
hot: true,
|
2016-01-29 18:14:37 +00:00
|
|
|
entryFile: filename,
|
|
|
|
recursive: true,
|
|
|
|
}).then(response => {
|
|
|
|
const module = packagerServer.getModuleForPath(filename);
|
|
|
|
|
2016-02-08 19:18:25 +00:00
|
|
|
return response.copy({dependencies: [module]});
|
2016-01-29 18:14:37 +00:00
|
|
|
});
|
2016-01-07 16:48:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// if there're new dependencies compare the full list of
|
|
|
|
// dependencies we used to have with the one we now have
|
|
|
|
return getDependencies(client.platform, client.bundleEntry)
|
|
|
|
.then(({
|
2016-03-17 22:05:55 +00:00
|
|
|
dependenciesCache: depsCache,
|
|
|
|
dependenciesModulesCache: depsModulesCache,
|
|
|
|
shallowDependencies: shallowDeps,
|
|
|
|
inverseDependenciesCache: inverseDepsCache,
|
2016-01-29 18:14:37 +00:00
|
|
|
resolutionResponse,
|
2016-01-07 16:48:39 +00:00
|
|
|
}) => {
|
|
|
|
if (!client) {
|
2016-01-29 18:14:37 +00:00
|
|
|
return {};
|
2016-01-04 21:01:28 +00:00
|
|
|
}
|
|
|
|
|
2016-01-07 16:48:39 +00:00
|
|
|
// build list of modules for which we'll send HMR updates
|
2016-01-15 18:51:56 +00:00
|
|
|
const modulesToUpdate = [packagerServer.getModuleForPath(filename)];
|
2016-03-12 00:16:05 +00:00
|
|
|
Object.keys(depsModulesCache).forEach(module => {
|
2016-03-17 22:05:55 +00:00
|
|
|
if (!client.dependenciesModulesCache[module]) {
|
2016-03-12 00:16:05 +00:00
|
|
|
modulesToUpdate.push(depsModulesCache[module]);
|
2016-01-07 16:48:39 +00:00
|
|
|
}
|
|
|
|
});
|
2016-01-04 21:01:28 +00:00
|
|
|
|
2016-03-04 00:06:30 +00:00
|
|
|
// Need to send modules to the client in an order it can
|
|
|
|
// process them: if a new dependency graph was uncovered
|
|
|
|
// because a new dependency was added, the file that was
|
|
|
|
// changed, which is the root of the dependency tree that
|
|
|
|
// will be sent, needs to be the last module that gets
|
|
|
|
// processed. Reversing the new modules makes sense
|
|
|
|
// because we get them through the resolver which returns
|
|
|
|
// a BFS ordered list.
|
|
|
|
modulesToUpdate.reverse();
|
|
|
|
|
2016-01-07 16:48:39 +00:00
|
|
|
// invalidate caches
|
2016-03-12 00:16:05 +00:00
|
|
|
client.dependenciesCache = depsCache;
|
|
|
|
client.dependenciesModulesCache = depsModulesCache;
|
|
|
|
client.shallowDependencies = shallowDeps;
|
|
|
|
client.inverseDependenciesCache = inverseDepsCache;
|
2016-01-07 16:48:39 +00:00
|
|
|
|
2016-02-08 19:18:25 +00:00
|
|
|
return resolutionResponse.copy({
|
|
|
|
dependencies: modulesToUpdate
|
|
|
|
});
|
2016-01-07 16:48:39 +00:00
|
|
|
});
|
|
|
|
})
|
2016-02-08 19:18:25 +00:00
|
|
|
.then((resolutionResponse) => {
|
2016-01-07 16:48:39 +00:00
|
|
|
if (!client) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// make sure the file was modified is part of the bundle
|
|
|
|
if (!client.shallowDependencies[filename]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-26 17:14:18 +00:00
|
|
|
const httpServerAddress = httpServer.address();
|
|
|
|
|
|
|
|
// Sanitize the value from the HTTP server
|
|
|
|
let packagerHost = 'localhost';
|
|
|
|
if (httpServer.address().address &&
|
|
|
|
httpServer.address().address !== '::' &&
|
|
|
|
httpServer.address().address !== '') {
|
|
|
|
packagerHost = httpServerAddress.address;
|
|
|
|
}
|
|
|
|
|
2016-01-07 16:48:39 +00:00
|
|
|
return packagerServer.buildBundleForHMR({
|
|
|
|
entryFile: client.bundleEntry,
|
|
|
|
platform: client.platform,
|
2016-01-29 18:14:37 +00:00
|
|
|
resolutionResponse,
|
2016-03-12 00:16:05 +00:00
|
|
|
}, packagerHost, httpServerAddress.port);
|
2016-01-07 16:48:39 +00:00
|
|
|
})
|
2016-01-20 15:13:26 +00:00
|
|
|
.then(bundle => {
|
2016-01-27 22:55:02 +00:00
|
|
|
if (!client || !bundle || bundle.isEmpty()) {
|
2016-01-07 21:14:18 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-01-27 22:55:02 +00:00
|
|
|
return JSON.stringify({
|
|
|
|
type: 'update',
|
|
|
|
body: {
|
2016-03-21 22:50:15 +00:00
|
|
|
modules: bundle.getModulesIdsAndCode(),
|
2016-03-12 00:16:05 +00:00
|
|
|
inverseDependencies: client.inverseDependenciesCache,
|
2016-01-27 22:55:02 +00:00
|
|
|
sourceURLs: bundle.getSourceURLs(),
|
|
|
|
sourceMappingURLs: bundle.getSourceMappingURLs(),
|
|
|
|
},
|
|
|
|
});
|
2016-01-07 21:14:18 +00:00
|
|
|
})
|
|
|
|
.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 JSON.stringify({type: 'error', body});
|
|
|
|
})
|
2016-01-20 15:13:26 +00:00
|
|
|
.then(update => {
|
2016-01-27 22:55:02 +00:00
|
|
|
if (!client || !update) {
|
2016-01-07 16:48:39 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-02-26 16:51:28 +00:00
|
|
|
console.log(
|
|
|
|
'[Hot Module Replacement] Sending HMR update to client (' +
|
|
|
|
time() + ')'
|
|
|
|
);
|
2016-01-20 15:13:26 +00:00
|
|
|
client.ws.send(update);
|
2016-01-06 17:46:56 +00:00
|
|
|
});
|
2016-01-07 16:48:39 +00:00
|
|
|
},
|
|
|
|
() => {
|
|
|
|
// do nothing, file was removed
|
|
|
|
},
|
2016-03-13 01:32:10 +00:00
|
|
|
).then(() => {
|
2016-02-01 20:41:04 +00:00
|
|
|
client.ws.send(JSON.stringify({type: 'update-done'}));
|
|
|
|
});
|
2015-12-30 02:24:10 +00:00
|
|
|
});
|
|
|
|
|
2015-12-30 02:24:08 +00:00
|
|
|
client.ws.on('error', e => {
|
|
|
|
console.error('[Hot Module Replacement] Unexpected error', e);
|
|
|
|
disconnect();
|
|
|
|
});
|
|
|
|
|
|
|
|
client.ws.on('close', () => disconnect());
|
|
|
|
})
|
|
|
|
.done();
|
2015-12-29 00:43:08 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-12-30 02:24:08 +00:00
|
|
|
function arrayEquals(arrayA, arrayB) {
|
|
|
|
arrayA = arrayA || [];
|
|
|
|
arrayB = arrayB || [];
|
|
|
|
return (
|
|
|
|
arrayA.length === arrayB.length &&
|
|
|
|
arrayA.every((element, index) => {
|
|
|
|
return element === arrayB[index];
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-02-26 16:51:28 +00:00
|
|
|
function time() {
|
|
|
|
const date = new Date();
|
|
|
|
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${date.getMilliseconds()}`;
|
|
|
|
}
|
|
|
|
|
2015-12-29 00:43:08 +00:00
|
|
|
module.exports = attachHMRServer;
|