/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*
*/
"use strict";
function _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === "function") {
ownKeys = ownKeys.concat(
Object.getOwnPropertySymbols(source).filter(function(sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
})
);
}
ownKeys.forEach(function(key) {
_defineProperty(target, key, source[key]);
});
}
return target;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _toConsumableArray(arr) {
return (
_arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread()
);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance");
}
function _iterableToArray(iter) {
if (
Symbol.iterator in Object(iter) ||
Object.prototype.toString.call(iter) === "[object Arguments]"
)
return Array.from(iter);
}
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++)
arr2[i] = arr[i];
return arr2;
}
}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _asyncToGenerator(fn) {
return function() {
var self = this,
args = arguments;
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
const GraphNotFoundError = require("./IncrementalBundler/GraphNotFoundError");
const IncrementalBundler = require("./IncrementalBundler");
const RevisionNotFoundError = require("./IncrementalBundler/RevisionNotFoundError");
const debounceAsyncQueue = require("./lib/debounceAsyncQueue");
const formatBundlingError = require("./lib/formatBundlingError");
const getGraphId = require("./lib/getGraphId");
const hmrJSBundle = require("./DeltaBundler/Serializers/hmrJSBundle");
const nullthrows = require("nullthrows");
const parseOptionsFromUrl = require("./lib/parseOptionsFromUrl");
const splitBundleOptions = require("./lib/splitBundleOptions");
const transformHelpers = require("./lib/transformHelpers");
const url = require("url");
const _require = require("metro-core"),
_require$Logger = _require.Logger,
createActionStartEntry = _require$Logger.createActionStartEntry,
createActionEndEntry = _require$Logger.createActionEndEntry,
log = _require$Logger.log;
function send(sendFns, message) {
const strMessage = JSON.stringify(message);
sendFns.forEach(sendFn => sendFn(strMessage));
}
/**
* The HmrServer (Hot Module Reloading) implements a lightweight interface
* to communicate easily to the logic in the React Native repository (which
* is the one that handles the Web Socket connections).
*
* This interface allows the HmrServer to hook its own logic to WS clients
* getting connected, disconnected or having errors (through the
* `onClientConnect`, `onClientDisconnect` and `onClientError` methods).
*/
class HmrServer {
constructor(bundler, createModuleId, config) {
this._config = config;
this._bundler = bundler;
this._createModuleId = createModuleId;
this._clientGroups = new Map();
}
onClientConnect(clientUrl, sendFn) {
var _this = this;
return _asyncToGenerator(function*() {
const urlObj = nullthrows(url.parse(clientUrl, true));
const query = nullthrows(urlObj.query);
let revPromise;
if (query.bundleEntry != null) {
// TODO(T34760695): Deprecate
urlObj.pathname = query.bundleEntry.replace(/\.js$/, ".bundle");
delete query.bundleEntry;
const _parseOptionsFromUrl = parseOptionsFromUrl(
url.format(urlObj),
new Set(_this._config.resolver.platforms)
),
options = _parseOptionsFromUrl.options;
const _splitBundleOptions = splitBundleOptions(options),
entryFile = _splitBundleOptions.entryFile,
transformOptions = _splitBundleOptions.transformOptions;
/**
* `entryFile` is relative to projectRoot, we need to use resolution function
* to find the appropriate file with supported extensions.
*/
const resolutionFn = yield transformHelpers.getResolveDependencyFn(
_this._bundler.getBundler(),
transformOptions.platform
);
const resolvedEntryFilePath = resolutionFn(
`${_this._config.projectRoot}/.`,
entryFile
);
const graphId = getGraphId(resolvedEntryFilePath, transformOptions);
revPromise = _this._bundler.getRevisionByGraphId(graphId);
if (!revPromise) {
send([sendFn], {
type: "error",
body: formatBundlingError(new GraphNotFoundError(graphId))
});
return null;
}
} else {
const revisionId = query.revisionId;
revPromise = _this._bundler.getRevision(revisionId);
if (!revPromise) {
send([sendFn], {
type: "error",
body: formatBundlingError(new RevisionNotFoundError(revisionId))
});
return null;
}
}
const _ref = yield revPromise,
graph = _ref.graph,
id = _ref.id;
const client = {
sendFn,
revisionId: id
};
let clientGroup = _this._clientGroups.get(id);
if (clientGroup != null) {
clientGroup.clients.add(client);
} else {
clientGroup = {
clients: new Set([client]),
unlisten: () => unlisten(),
revisionId: id
};
_this._clientGroups.set(id, clientGroup);
const unlisten = _this._bundler
.getDeltaBundler()
.listen(
graph,
debounceAsyncQueue(
_this._handleFileChange.bind(_this, clientGroup),
50
)
);
}
yield _this._handleFileChange(clientGroup);
return client;
})();
}
onClientError(client, e) {
this._config.reporter.update({
type: "hmr_client_error",
error: e
});
this.onClientDisconnect(client);
}
onClientDisconnect(client) {
const group = this._clientGroups.get(client.revisionId);
if (group != null) {
if (group.clients.size === 1) {
this._clientGroups.delete(client.revisionId);
group.unlisten();
} else {
group.clients.delete(client);
}
}
}
_handleFileChange(group) {
var _this2 = this;
return _asyncToGenerator(function*() {
const processingHmrChange = log(
createActionStartEntry({
action_name: "Processing HMR change"
})
);
const sendFns = _toConsumableArray(group.clients).map(
client => client.sendFn
);
send(sendFns, {
type: "update-start"
});
const message = yield _this2._prepareMessage(group);
send(sendFns, message);
send(sendFns, {
type: "update-done"
});
log(
_objectSpread({}, createActionEndEntry(processingHmrChange), {
outdated_modules:
message.type === "update"
? message.body.added.length + message.body.modified.length
: undefined
})
);
})();
}
_prepareMessage(group) {
var _this3 = this;
return _asyncToGenerator(function*() {
try {
const revPromise = _this3._bundler.getRevision(group.revisionId);
if (!revPromise) {
return {
type: "error",
body: formatBundlingError(
new RevisionNotFoundError(group.revisionId)
)
};
}
const _ref2 = yield _this3._bundler.updateGraph(
yield revPromise,
false
),
revision = _ref2.revision,
delta = _ref2.delta;
_this3._clientGroups.delete(group.revisionId);
group.revisionId = revision.id;
for (const client of group.clients) {
client.revisionId = revision.id;
}
_this3._clientGroups.set(group.revisionId, group);
const hmrUpdate = hmrJSBundle(delta, revision.graph, {
createModuleId: _this3._createModuleId,
projectRoot: _this3._config.projectRoot
});
return {
type: "update",
body: _objectSpread(
{
revisionId: revision.id
},
hmrUpdate
)
};
} catch (error) {
const formattedError = formatBundlingError(error);
_this3._config.reporter.update({
type: "bundling_error",
error
});
return {
type: "error",
body: formattedError
};
}
})();
}
}
module.exports = HmrServer;