1/**
2 * Copyright (c) 650 Industries.
3 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 *
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the root directory of this source tree.
7 *
8 * Based on this but with web support:
9 * https://github.com/facebook/react-native/blob/086714b02b0fb838dee5a66c5bcefe73b53cf3df/Libraries/Utilities/HMRClient.js
10 */
11import prettyFormat, { plugins } from 'pretty-format';
12import LoadingView from './LoadingView';
13import LogBox from './error-overlay/LogBox';
14import getDevServer from './getDevServer';
15const MetroHMRClient = require('metro-runtime/src/modules/HMRClient');
16const pendingEntryPoints = [];
17let hmrClient = null;
18let hmrUnavailableReason = null;
19let currentCompileErrorMessage = null;
20let didConnect = false;
21const pendingLogs = [];
22function assert(foo, msg) {
23    if (!foo)
24        throw new Error(msg);
25}
26/**
27 * HMR Client that receives from the server HMR updates and propagates them
28 * runtime to reflects those changes.
29 */
30const HMRClient = {
31    enable() {
32        if (hmrUnavailableReason !== null) {
33            // If HMR became unavailable while you weren't using it,
34            // explain why when you try to turn it on.
35            // This is an error (and not a warning) because it is shown
36            // in response to a direct user action.
37            throw new Error(hmrUnavailableReason);
38        }
39        assert(hmrClient, 'Expected HMRClient.setup() call at startup.');
40        // We use this for internal logging only.
41        // It doesn't affect the logic.
42        hmrClient.send(JSON.stringify({ type: 'log-opt-in' }));
43        // When toggling Fast Refresh on, we might already have some stashed updates.
44        // Since they'll get applied now, we'll show a banner.
45        const hasUpdates = hmrClient.hasPendingUpdates();
46        if (hasUpdates) {
47            LoadingView.showMessage('Refreshing...', 'refresh');
48        }
49        try {
50            hmrClient.enable();
51        }
52        finally {
53            if (hasUpdates) {
54                LoadingView.hide();
55            }
56        }
57        // There could be a compile error while Fast Refresh was off,
58        // but we ignored it at the time. Show it now.
59        showCompileError();
60    },
61    disable() {
62        assert(hmrClient, 'Expected HMRClient.setup() call at startup.');
63        hmrClient.disable();
64    },
65    registerBundle(requestUrl) {
66        assert(hmrClient, 'Expected HMRClient.setup() call at startup.');
67        pendingEntryPoints.push(requestUrl);
68        registerBundleEntryPoints(hmrClient);
69    },
70    log(level, data) {
71        if (!hmrClient) {
72            // Catch a reasonable number of early logs
73            // in case hmrClient gets initialized later.
74            pendingLogs.push([level, data]);
75            if (pendingLogs.length > 100) {
76                pendingLogs.shift();
77            }
78            return;
79        }
80        try {
81            hmrClient.send(JSON.stringify({
82                type: 'log',
83                level,
84                mode: 'BRIDGE',
85                data: data.map((item) => typeof item === 'string'
86                    ? item
87                    : prettyFormat(item, {
88                        escapeString: true,
89                        highlight: true,
90                        maxDepth: 3,
91                        min: true,
92                        plugins: [plugins.ReactElement],
93                    })),
94            }));
95        }
96        catch {
97            // If sending logs causes any failures we want to silently ignore them
98            // to ensure we do not cause infinite-logging loops.
99        }
100    },
101    // Called once by the bridge on startup, even if Fast Refresh is off.
102    // It creates the HMR client but doesn't actually set up the socket yet.
103    setup({ isEnabled }) {
104        assert(!hmrClient, 'Cannot initialize hmrClient twice');
105        const serverScheme = window.location.protocol === 'https:' ? 'wss' : 'ws';
106        const client = new MetroHMRClient(`${serverScheme}://${window.location.host}/hot`);
107        hmrClient = client;
108        const { fullBundleUrl } = getDevServer();
109        pendingEntryPoints.push(
110        // HMRServer understands regular bundle URLs, so prefer that in case
111        // there are any important URL parameters we can't reconstruct from
112        // `setup()`'s arguments.
113        fullBundleUrl);
114        client.on('connection-error', (e) => {
115            let error = `Cannot connect to Metro.
116
117 Try the following to fix the issue:
118 - Ensure the Metro dev server is running and available on the same network as this device`;
119            error += `
120
121 URL: ${window.location.host}
122
123 Error: ${e.message}`;
124            setHMRUnavailableReason(error);
125        });
126        client.on('update-start', ({ isInitialUpdate }) => {
127            currentCompileErrorMessage = null;
128            didConnect = true;
129            if (client.isEnabled() && !isInitialUpdate) {
130                LoadingView.showMessage('Refreshing...', 'refresh');
131            }
132        });
133        client.on('update', ({ isInitialUpdate }) => {
134            if (client.isEnabled() && !isInitialUpdate) {
135                dismissRedbox();
136                LogBox.clearAllLogs();
137            }
138        });
139        client.on('update-done', () => {
140            LoadingView.hide();
141        });
142        client.on('error', (data) => {
143            LoadingView.hide();
144            if (data.type === 'GraphNotFoundError') {
145                client.close();
146                setHMRUnavailableReason('Metro has restarted since the last edit. Reload to reconnect.');
147            }
148            else if (data.type === 'RevisionNotFoundError') {
149                client.close();
150                setHMRUnavailableReason('Metro and the client are out of sync. Reload to reconnect.');
151            }
152            else {
153                currentCompileErrorMessage = `${data.type} ${data.message}`;
154                if (client.isEnabled()) {
155                    showCompileError();
156                }
157            }
158        });
159        client.on('close', (closeEvent) => {
160            LoadingView.hide();
161            // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1
162            // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5
163            const isNormalOrUnsetCloseReason = closeEvent == null ||
164                closeEvent.code === 1000 ||
165                closeEvent.code === 1005 ||
166                closeEvent.code == null;
167            setHMRUnavailableReason(`${isNormalOrUnsetCloseReason
168                ? 'Disconnected from Metro.'
169                : `Disconnected from Metro (${closeEvent.code}: "${closeEvent.reason}").`}
170
171To reconnect:
172- Ensure that Metro is running and available on the same network
173- Reload this app (will trigger further help if Metro cannot be connected to)
174      `);
175        });
176        if (isEnabled) {
177            HMRClient.enable();
178        }
179        else {
180            HMRClient.disable();
181        }
182        registerBundleEntryPoints(hmrClient);
183        flushEarlyLogs();
184    },
185};
186function setHMRUnavailableReason(reason) {
187    assert(hmrClient, 'Expected HMRClient.setup() call at startup.');
188    if (hmrUnavailableReason !== null) {
189        // Don't show more than one warning.
190        return;
191    }
192    hmrUnavailableReason = reason;
193    // We only want to show a warning if Fast Refresh is on *and* if we ever
194    // previously managed to connect successfully. We don't want to show
195    // the warning to native engineers who use cached bundles without Metro.
196    if (hmrClient.isEnabled() && didConnect) {
197        console.warn(reason);
198        // (Not using the `warning` module to prevent a Buck cycle.)
199    }
200}
201function registerBundleEntryPoints(client) {
202    if (hmrUnavailableReason != null) {
203        // "Bundle Splitting – Metro disconnected"
204        window.location.reload();
205        return;
206    }
207    if (pendingEntryPoints.length > 0) {
208        client?.send(JSON.stringify({
209            type: 'register-entrypoints',
210            entryPoints: pendingEntryPoints,
211        }));
212        pendingEntryPoints.length = 0;
213    }
214}
215function flushEarlyLogs() {
216    try {
217        pendingLogs.forEach(([level, data]) => {
218            HMRClient.log(level, data);
219        });
220    }
221    finally {
222        pendingLogs.length = 0;
223    }
224}
225function dismissRedbox() {
226    // TODO(EvanBacon): Error overlay for web.
227}
228function showCompileError() {
229    if (currentCompileErrorMessage === null) {
230        return;
231    }
232    // Even if there is already a redbox, syntax errors are more important.
233    // Otherwise you risk seeing a stale runtime error while a syntax error is more recent.
234    dismissRedbox();
235    const message = currentCompileErrorMessage;
236    currentCompileErrorMessage = null;
237    const error = new Error(message);
238    // Symbolicating compile errors is wasted effort
239    // because the stack trace is meaningless:
240    // @ts-expect-error
241    error.preventSymbolication = true;
242    throw error;
243}
244export default HMRClient;
245//# sourceMappingURL=HMRClient.js.map