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