- Prefijo v2: MCH - TRACEABILITY-MASTER.yml creado - Listo para integracion como submodulo Workspace: v2.0.0 | SIMCO: v4.0.0
390 lines
12 KiB
Plaintext
390 lines
12 KiB
Plaintext
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow
|
|
*/
|
|
|
|
import type IncrementalBundler, {RevisionId} from './IncrementalBundler';
|
|
import type {GraphOptions} from './shared/types';
|
|
import type {ConfigT, RootPerfLogger} from 'metro-config';
|
|
import type {
|
|
HmrClientMessage,
|
|
HmrErrorMessage,
|
|
HmrMessage,
|
|
HmrUpdateMessage,
|
|
} from 'metro-runtime/src/modules/types';
|
|
|
|
import hmrJSBundle from './DeltaBundler/Serializers/hmrJSBundle';
|
|
import GraphNotFoundError from './IncrementalBundler/GraphNotFoundError';
|
|
import RevisionNotFoundError from './IncrementalBundler/RevisionNotFoundError';
|
|
import debounceAsyncQueue from './lib/debounceAsyncQueue';
|
|
import formatBundlingError from './lib/formatBundlingError';
|
|
import getGraphId from './lib/getGraphId';
|
|
import parseBundleOptionsFromBundleRequestUrl from './lib/parseBundleOptionsFromBundleRequestUrl';
|
|
import splitBundleOptions from './lib/splitBundleOptions';
|
|
import * as transformHelpers from './lib/transformHelpers';
|
|
import {Logger} from 'metro-core';
|
|
import nullthrows from 'nullthrows';
|
|
|
|
// eslint-disable-next-line import/no-commonjs
|
|
const debug = require('debug')('Metro:HMR');
|
|
|
|
const {createActionStartEntry, createActionEndEntry, log} = Logger;
|
|
|
|
export type Client = {
|
|
optedIntoHMR: boolean,
|
|
revisionIds: Array<RevisionId>,
|
|
+sendFn: string => void,
|
|
};
|
|
|
|
type ClientGroup = {
|
|
+clients: Set<Client>,
|
|
clientUrl: URL,
|
|
revisionId: RevisionId,
|
|
+unlisten: () => void,
|
|
+graphOptions: GraphOptions,
|
|
};
|
|
|
|
function send(sendFns: Array<(string) => void>, message: HmrMessage): void {
|
|
const strMessage = JSON.stringify(message);
|
|
sendFns.forEach((sendFn: string => void) => 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).
|
|
*/
|
|
export default class HmrServer<TClient: Client> {
|
|
_config: ConfigT;
|
|
_bundler: IncrementalBundler;
|
|
_createModuleId: (path: string) => number;
|
|
_clientGroups: Map<RevisionId, ClientGroup>;
|
|
|
|
constructor(
|
|
bundler: IncrementalBundler,
|
|
createModuleId: (path: string) => number,
|
|
config: ConfigT,
|
|
) {
|
|
this._config = config;
|
|
this._bundler = bundler;
|
|
this._createModuleId = createModuleId;
|
|
this._clientGroups = new Map();
|
|
}
|
|
|
|
onClientConnect: (
|
|
requestUrl: string,
|
|
sendFn: (data: string) => void,
|
|
) => Promise<Client> = async (requestUrl, sendFn) => {
|
|
return {
|
|
sendFn,
|
|
revisionIds: [],
|
|
optedIntoHMR: false,
|
|
};
|
|
};
|
|
|
|
async _registerEntryPoint(
|
|
client: Client,
|
|
requestUrl: string,
|
|
sendFn: (data: string) => void,
|
|
): Promise<void> {
|
|
debug('Registering entry point: %s', requestUrl);
|
|
requestUrl = this._config.server.rewriteRequestUrl(requestUrl);
|
|
debug('Rewritten as: %s', requestUrl);
|
|
|
|
const {bundleType: _bundleType, ...options} =
|
|
parseBundleOptionsFromBundleRequestUrl(
|
|
requestUrl,
|
|
new Set(this._config.resolver.platforms),
|
|
);
|
|
const {entryFile, resolverOptions, transformOptions, graphOptions} =
|
|
splitBundleOptions(options);
|
|
|
|
/**
|
|
* `entryFile` is relative to projectRoot, we need to use resolution function
|
|
* to find the appropriate file with supported extensions.
|
|
*/
|
|
const resolutionFn = await transformHelpers.getResolveDependencyFn(
|
|
this._bundler.getBundler(),
|
|
transformOptions.platform,
|
|
resolverOptions,
|
|
);
|
|
const resolvedEntryFilePath = resolutionFn(
|
|
(this._config.server.unstable_serverRoot ?? this._config.projectRoot) +
|
|
'/.',
|
|
{
|
|
name: entryFile,
|
|
data: {
|
|
key: entryFile,
|
|
asyncType: null,
|
|
isESMImport: false,
|
|
locs: [],
|
|
},
|
|
},
|
|
).filePath;
|
|
const graphId = getGraphId(resolvedEntryFilePath, transformOptions, {
|
|
resolverOptions,
|
|
shallow: graphOptions.shallow,
|
|
lazy: graphOptions.lazy,
|
|
unstable_allowRequireContext:
|
|
this._config.transformer.unstable_allowRequireContext,
|
|
});
|
|
const revPromise = this._bundler.getRevisionByGraphId(graphId);
|
|
if (!revPromise) {
|
|
send([sendFn], {
|
|
type: 'error',
|
|
body: formatBundlingError(new GraphNotFoundError(graphId)),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const {graph, id} = await revPromise;
|
|
client.revisionIds.push(id);
|
|
|
|
let clientGroup: ?ClientGroup = this._clientGroups.get(id);
|
|
if (clientGroup != null) {
|
|
clientGroup.clients.add(client);
|
|
} else {
|
|
const clientUrl = new URL(requestUrl);
|
|
|
|
// Prepare the clientUrl to be used as sourceUrl in HMR updates.
|
|
clientUrl.protocol = 'http';
|
|
|
|
const clientQuery = clientUrl.searchParams;
|
|
clientQuery.delete('bundleEntry');
|
|
clientQuery.set('dev', clientQuery.get('dev') || 'true');
|
|
clientQuery.set('minify', clientQuery.get('minify') || 'false');
|
|
clientQuery.set('modulesOnly', 'true');
|
|
clientQuery.set('runModule', clientQuery.get('runModule') || 'false');
|
|
clientQuery.set('shallow', 'true');
|
|
|
|
clientGroup = {
|
|
clients: new Set([client]),
|
|
clientUrl: new URL(clientUrl),
|
|
revisionId: id,
|
|
graphOptions,
|
|
unlisten: (): void => unlisten(),
|
|
};
|
|
|
|
this._clientGroups.set(id, clientGroup);
|
|
|
|
let latestEventArgs: Array<any> = [];
|
|
|
|
const debounceCallHandleFileChange = debounceAsyncQueue(async () => {
|
|
await this._handleFileChange(
|
|
nullthrows(clientGroup),
|
|
{isInitialUpdate: false},
|
|
...latestEventArgs,
|
|
);
|
|
}, 50);
|
|
|
|
const unlisten = this._bundler
|
|
.getDeltaBundler()
|
|
// $FlowFixMe[missing-local-annot]
|
|
.listen(graph, async (...args) => {
|
|
latestEventArgs = args;
|
|
await debounceCallHandleFileChange();
|
|
});
|
|
}
|
|
|
|
await this._handleFileChange(clientGroup, {isInitialUpdate: true});
|
|
send([sendFn], {type: 'bundle-registered'});
|
|
}
|
|
|
|
onClientMessage: (
|
|
client: TClient,
|
|
message: string | Buffer | ArrayBuffer | Array<Buffer>,
|
|
sendFn: (data: string) => void,
|
|
) => Promise<void> = async (client, message, sendFn) => {
|
|
let data: HmrClientMessage;
|
|
try {
|
|
data = JSON.parse(String(message));
|
|
} catch (error) {
|
|
send([sendFn], {
|
|
type: 'error',
|
|
body: formatBundlingError(error),
|
|
});
|
|
return Promise.resolve();
|
|
}
|
|
if (data && data.type) {
|
|
switch (data.type) {
|
|
case 'register-entrypoints':
|
|
return Promise.all(
|
|
data.entryPoints.map(entryPoint =>
|
|
this._registerEntryPoint(client, entryPoint, sendFn),
|
|
),
|
|
);
|
|
case 'log':
|
|
if (this._config.server.forwardClientLogs) {
|
|
this._config.reporter.update({
|
|
type: 'client_log',
|
|
level: data.level,
|
|
data: data.data,
|
|
mode: data.mode,
|
|
});
|
|
}
|
|
break;
|
|
case 'log-opt-in':
|
|
client.optedIntoHMR = true;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
onClientError: (client: TClient, e: ErrorEvent) => void = (client, e) => {
|
|
this._config.reporter.update({
|
|
type: 'hmr_client_error',
|
|
error: e.error,
|
|
});
|
|
this.onClientDisconnect(client);
|
|
};
|
|
|
|
onClientDisconnect: (client: TClient) => void = client => {
|
|
client.revisionIds.forEach(revisionId => {
|
|
const group = this._clientGroups.get(revisionId);
|
|
if (group != null) {
|
|
if (group.clients.size === 1) {
|
|
this._clientGroups.delete(revisionId);
|
|
group.unlisten();
|
|
} else {
|
|
group.clients.delete(client);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
async _handleFileChange(
|
|
group: ClientGroup,
|
|
options: {isInitialUpdate: boolean},
|
|
changeEvent: ?{
|
|
logger: ?RootPerfLogger,
|
|
},
|
|
): Promise<void> {
|
|
const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
|
|
if (logger) {
|
|
logger.point('fileChange_end');
|
|
logger.point('hmrPrepareAndSendMessage_start');
|
|
}
|
|
|
|
const optedIntoHMR = [...group.clients].some(
|
|
(client: Client) => client.optedIntoHMR,
|
|
);
|
|
const processingHmrChange = log(
|
|
createActionStartEntry({
|
|
// Even when HMR is disabled on the client, this function still
|
|
// runs so we can stash updates while it's off and apply them later.
|
|
// However, this would mess up our internal analytics because we track
|
|
// HMR as being used even for people who have it disabled.
|
|
// As a workaround, we use a different event name for clients
|
|
// that didn't explicitly opt into HMR.
|
|
action_name: optedIntoHMR
|
|
? 'Processing HMR change'
|
|
: 'Processing HMR change (no client opt-in)',
|
|
}),
|
|
);
|
|
|
|
const sendFns = [...group.clients].map((client: Client) => client.sendFn);
|
|
|
|
send(sendFns, {
|
|
type: 'update-start',
|
|
body: options,
|
|
});
|
|
|
|
const message = await this._prepareMessage(group, options, changeEvent);
|
|
send(sendFns, message);
|
|
send(sendFns, {type: 'update-done'});
|
|
|
|
log({
|
|
...createActionEndEntry(processingHmrChange),
|
|
outdated_modules:
|
|
message.type === 'update'
|
|
? message.body.added.length + message.body.modified.length
|
|
: undefined,
|
|
});
|
|
|
|
if (logger) {
|
|
logger.point('hmrPrepareAndSendMessage_end');
|
|
logger.end('SUCCESS');
|
|
}
|
|
}
|
|
|
|
async _prepareMessage(
|
|
group: ClientGroup,
|
|
options: {isInitialUpdate: boolean},
|
|
changeEvent: ?{
|
|
logger: ?RootPerfLogger,
|
|
},
|
|
): Promise<HmrUpdateMessage | HmrErrorMessage> {
|
|
const logger = !options.isInitialUpdate ? changeEvent?.logger : null;
|
|
try {
|
|
const revPromise = this._bundler.getRevision(group.revisionId);
|
|
if (!revPromise) {
|
|
return {
|
|
type: 'error',
|
|
body: formatBundlingError(
|
|
new RevisionNotFoundError(group.revisionId),
|
|
),
|
|
};
|
|
}
|
|
|
|
logger?.point('updateGraph_start');
|
|
|
|
const {revision, delta} = await this._bundler.updateGraph(
|
|
await revPromise,
|
|
false,
|
|
);
|
|
|
|
logger?.point('updateGraph_end');
|
|
|
|
this._clientGroups.delete(group.revisionId);
|
|
group.revisionId = revision.id;
|
|
for (const client of group.clients) {
|
|
client.revisionIds = client.revisionIds.filter(
|
|
revisionId => revisionId !== group.revisionId,
|
|
);
|
|
client.revisionIds.push(revision.id);
|
|
}
|
|
this._clientGroups.set(group.revisionId, group);
|
|
|
|
logger?.point('serialize_start');
|
|
|
|
const hmrUpdate = hmrJSBundle(delta, revision.graph, {
|
|
clientUrl: new URL(group.clientUrl),
|
|
createModuleId: this._createModuleId,
|
|
includeAsyncPaths: group.graphOptions.lazy,
|
|
projectRoot: this._config.projectRoot,
|
|
serverRoot:
|
|
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
|
|
});
|
|
|
|
logger?.point('serialize_end');
|
|
|
|
return {
|
|
type: 'update',
|
|
body: {
|
|
revisionId: revision.id,
|
|
isInitialUpdate: options.isInitialUpdate,
|
|
...hmrUpdate,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
const formattedError = formatBundlingError(error);
|
|
|
|
this._config.reporter.update({type: 'bundling_error', error});
|
|
|
|
return {type: 'error', body: formattedError};
|
|
}
|
|
}
|
|
}
|