import { ByteBuffer } from "peechy"; import * as API from "../api/schema"; var __HMRModule, __FastRefreshModule, __HMRClient, __injectFastRefresh; if (typeof window !== "undefined") { // We add a scope here to minimize chances of namespace collisions var runOnce = false; var clientStartTime = 0; function formatDuration(duration: number) { return Math.round(duration * 1000) / 1000; } enum CSSImportState { Pending, Loading, Loaded, } type HTMLStylableElement = HTMLLinkElement | HTMLStyleElement; type CSSHMRInsertionPoint = { id: number; node?: HTMLStylableElement; file: string; bundle_id: number; sheet: CSSStyleSheet; }; enum CSSUpdateMethod { // CSS OM allows synchronous style updates cssObjectModel, // Blob URLs allow us to skip converting to JavaScript strings // However, they run asynchronously. Frequent updates cause FOUC blobURL, } enum ReloadBehavior { fullReload, hotReload, ignore, } const FastRefreshLoader = { RefreshRuntime: null, isUpdateInProgress: false, hasInjectedFastRefresh: false, performFullRefresh() { HMRClient.client.performFullReload(); }, async hotReload() { if (FastRefreshLoader.isUpdateInProgress) return; try { FastRefreshLoader.isUpdateInProgress = true; } finally { FastRefreshLoader.isUpdateInProgress = false; } }, }; const BunError = { module: null, prom: null, cancel: false, lastError: null, render(error, cwd) { if ("__BunRenderBuildError" in globalThis) { globalThis.__BunRenderBuildError(error, cwd); return; } BunError.lastError = [error, cwd]; BunError.cancel = false; if (!BunError.module) { if (BunError.prom) return; BunError.prom = import("/bun:error.js").then((mod) => { BunError.module = mod; !BunError.cancel && BunError.render(BunError.lastError[0], BunError.lastError[1]); }); return; } const { renderBuildFailure } = BunError.module; renderBuildFailure(BunError.lastError[0], BunError.lastError[1]); }, clear() { BunError.lastError = null; BunError.cancel = true; if (BunError.module) { const { clearBuildFailure } = BunError.module; clearBuildFailure(); } else if ("__BunClearBuildFailure" in globalThis) { globalThis.__BunClearBuildFailure(); } }, }; class CSSLoader { hmr: HMRClient; private static cssLoadId: CSSHMRInsertionPoint = { id: 0, bundle_id: 0, node: null, file: "", sheet: null, }; updateMethod: CSSUpdateMethod; decoder: TextDecoder; constructor() { if ("replaceSync" in CSSStyleSheet.prototype) { this.updateMethod = CSSUpdateMethod.cssObjectModel; } else { this.updateMethod = CSSUpdateMethod.blobURL; } } // This is a separate function because calling a small function 2000 times is more likely to cause it to be JIT'd // We want it to be JIT'd // It's possible that returning null may be a de-opt though. private findMatchingSupportsRule( rule: CSSSupportsRule, id: number, sheet: CSSStyleSheet ): CSSHMRInsertionPoint | null { switch (rule.type) { // 12 is result.SUPPORTS_RULE case 12: { if (!rule.conditionText.startsWith("(hmr-wid:")) { return null; } const startIndex = "hmr-wid:".length + 1; const endIDRegion = rule.conditionText.indexOf(")", startIndex); if (endIDRegion === -1) return null; const int = parseInt( rule.conditionText.substring(startIndex, endIDRegion), 10 ); if (int !== id) { return null; } let startFileRegion = rule.conditionText.indexOf( '(hmr-file:"', endIDRegion ); if (startFileRegion === -1) return null; startFileRegion += '(hmr-file:"'.length + 1; const endFileRegion = rule.conditionText.indexOf( '"', startFileRegion ); if (endFileRegion === -1) return null; // Empty file strings are invalid if (endFileRegion - startFileRegion <= 0) return null; CSSLoader.cssLoadId.id = int; CSSLoader.cssLoadId.node = sheet.ownerNode as HTMLStylableElement; CSSLoader.cssLoadId.sheet = sheet; CSSLoader.cssLoadId.file = rule.conditionText.substring( startFileRegion - 1, endFileRegion ); return CSSLoader.cssLoadId; } default: { return null; } } } bundleId(): number { return CSSLoader.cssLoadId.bundle_id; } private findCSSLinkTag(id: number): CSSHMRInsertionPoint | null { let count = 0; let match: CSSHMRInsertionPoint = null; const adoptedStyles = document.adoptedStyleSheets; if (this.updateMethod === CSSUpdateMethod.cssObjectModel) { if (adoptedStyles.length > 0) { count = adoptedStyles.length; for (let i = 0; i < count && match === null; i++) { let cssRules: CSSRuleList; let sheet: CSSStyleSheet; let ruleCount = 0; // Non-same origin stylesheets will potentially throw "Security error" // We will ignore those stylesheets and look at others. try { sheet = adoptedStyles[i]; cssRules = sheet.rules; ruleCount = sheet.rules.length; } catch (exception) { continue; } if (sheet.disabled || sheet.rules.length === 0) { continue; } const bundleIdRule = cssRules[0] as CSSSupportsRule; if ( bundleIdRule.type !== 12 || !bundleIdRule.conditionText.startsWith("(hmr-bid:") ) { continue; } const bundleIdEnd = bundleIdRule.conditionText.indexOf( ")", "(hmr-bid:".length + 1 ); if (bundleIdEnd === -1) continue; CSSLoader.cssLoadId.bundle_id = parseInt( bundleIdRule.conditionText.substring( "(hmr-bid:".length, bundleIdEnd ), 10 ); for (let j = 1; j < ruleCount && match === null; j++) { match = this.findMatchingSupportsRule( cssRules[j] as CSSSupportsRule, id, sheet ); } } } } count = document.styleSheets.length; for (let i = 0; i < count && match === null; i++) { let cssRules: CSSRuleList; let sheet: CSSStyleSheet; let ruleCount = 0; // Non-same origin stylesheets will potentially throw "Security error" // We will ignore those stylesheets and look at others. try { sheet = document.styleSheets.item(i); cssRules = sheet.rules; ruleCount = sheet.rules.length; } catch (exception) { continue; } if (sheet.disabled || sheet.rules.length === 0) { continue; } const bundleIdRule = cssRules[0] as CSSSupportsRule; if ( bundleIdRule.type !== 12 || !bundleIdRule.conditionText.startsWith("(hmr-bid:") ) { continue; } const bundleIdEnd = bundleIdRule.conditionText.indexOf( ")", "(hmr-bid:".length + 1 ); if (bundleIdEnd === -1) continue; CSSLoader.cssLoadId.bundle_id = parseInt( bundleIdRule.conditionText.substring("(hmr-bid:".length, bundleIdEnd), 10 ); for (let j = 1; j < ruleCount && match === null; j++) { match = this.findMatchingSupportsRule( cssRules[j] as CSSSupportsRule, id, sheet ); } } // Ensure we don't leak the HTMLLinkElement if (match === null) { CSSLoader.cssLoadId.file = ""; CSSLoader.cssLoadId.bundle_id = CSSLoader.cssLoadId.id = 0; CSSLoader.cssLoadId.node = null; CSSLoader.cssLoadId.sheet = null; } return match; } handleBuildSuccess( buffer: ByteBuffer, build: API.WebsocketMessageBuildSuccess, timestamp: number ) { const start = performance.now(); var update = this.findCSSLinkTag(build.id); let bytes = buffer.data.length > buffer.index ? buffer.data.subarray(buffer.index) : new Uint8Array(0); if (update === null) { __hmrlog.debug("Skipping unused CSS."); return; } if (bytes.length === 0) { __hmrlog.debug("Skipping empty file"); return; } let filepath = update.file; if (filepath.startsWith(this.hmr.cwd)) { filepath = filepath.substring(this.hmr.cwd.length); } const _timestamp = timestamp; const from_timestamp = build.from_timestamp; function onLoadHandler() { const localDuration = formatDuration(performance.now() - start); const fsDuration = _timestamp - from_timestamp; __hmrlog.log( "Reloaded in", `${localDuration + fsDuration}ms`, "-", filepath ); update = null; filepath = null; } // Whenever switch (this.updateMethod) { case CSSUpdateMethod.blobURL: { let blob = new Blob([bytes], { type: "text/css" }); const blobURL = URL.createObjectURL(blob); // onLoad doesn't fire in Chrome. // I'm not sure why. // Guessing it only triggers when an element is added/removed, not when the href just changes // So we say on the next tick, we're loaded. setTimeout(onLoadHandler.bind(update.node), 0); update.node.setAttribute("href", blobURL); blob = null; URL.revokeObjectURL(blobURL); break; } case CSSUpdateMethod.cssObjectModel: { if (!this.decoder) { this.decoder = new TextDecoder("UTF8"); } // This is an adoptedStyleSheet, call replaceSync and be done with it. if (!update.node || update.node.tagName === "HTML") { update.sheet.replaceSync(this.decoder.decode(bytes)); } else if ( update.node.tagName === "LINK" || update.node.tagName === "STYLE" ) { // This might cause CSS specifity issues.... // I'm not 100% sure this is a safe operation const sheet = new CSSStyleSheet(); sheet.replaceSync(this.decoder.decode(bytes)); update.node.remove(); document.adoptedStyleSheets = [ ...document.adoptedStyleSheets, sheet, ]; } break; } } buffer = null; bytes = null; } filePath( file_change_notification: API.WebsocketMessageFileChangeNotification ): string | null { if (file_change_notification.loader !== API.Loader.css) return null; const tag = this.findCSSLinkTag(file_change_notification.id); if (!tag) { return null; } return tag.file; } } class HMRClient { static client: HMRClient; socket: WebSocket; hasWelcomed: boolean = false; reconnect: number = 0; // Server timestamps are relative to the time the server's HTTP server launched // This so we can send timestamps as uint32 instead of 128-bit integers epoch: number = 0; javascriptReloader: API.Reloader = API.Reloader.disable; loaders = { css: new CSSLoader(), }; sessionId: number; start() { if (runOnce) { __hmrlog.warn( "Attempted to start HMR client multiple times. This may be a bug." ); return; } this.loaders.css.hmr = this; runOnce = true; this.connect(); // Explicitly send a socket close event so the thread doesn't have to wait for a timeout var origUnload = globalThis.onbeforeunload; globalThis.onbeforeunload = (ev: Event) => { this.disableReconnect = true; if (this.socket && this.socket.readyState === this.socket.OPEN) { this.socket.close(4990, "unload"); } origUnload && origUnload.call(globalThis, [ev]); }; } nextReconnectAttempt = 0; reconnectDelay = 16; debouncedReconnect = () => { if ( this.socket && (this.socket.readyState == this.socket.OPEN || this.socket.readyState == this.socket.CONNECTING) ) return; this.nextReconnectAttempt = setTimeout( this.attemptReconnect, this.reconnectDelay ); }; attemptReconnect = () => { globalThis.clearTimeout(this.nextReconnectAttempt); if ( this.socket && (this.socket.readyState == this.socket.OPEN || this.socket.readyState == this.socket.CONNECTING) ) return; this.connect(); this.reconnectDelay += Math.floor(Math.random() * 128); }; connect() { if ( this.socket && (this.socket.readyState == this.socket.OPEN || this.socket.readyState == this.socket.CONNECTING) ) return; clientStartTime = performance.now(); const baseURL = new URL(location.origin + "/bun:_api.hmr"); baseURL.protocol = location.protocol === "https:" ? "wss" : "ws"; this.socket = new WebSocket(baseURL.toString(), ["bun-hmr"]); this.socket.binaryType = "arraybuffer"; this.socket.onclose = this.handleClose; this.socket.onerror = this.handleError; this.socket.onopen = this.handleOpen; this.socket.onmessage = this.handleMessage; } // key: module id // value: server-timestamp builds = new Map(); cwd: string; indexOfModuleId(id: number): number { return HMRModule.dependencies.graph.indexOf(id); } static cssQueue = []; static cssState = CSSImportState.Pending; static cssAutoFOUC = false; static processPendingCSSImports() { const pending = HMRClient.cssQueue.slice(); HMRClient.cssQueue.length = 0; return Promise.all(pending).then(() => { if (HMRClient.cssQueue.length > 0) { const _pending = HMRClient.cssQueue.slice(); HMRClient.cssQueue.length = 0; return Promise.all(_pending).then(HMRClient.processPendingCSSImports); } else { return true; } }); } static importCSS(promise: Promise) { switch (HMRClient.cssState) { case CSSImportState.Pending: { this.cssState = CSSImportState.Loading; // This means we can import without risk of FOUC if ( document.documentElement.innerText === "" && !HMRClient.cssAutoFOUC ) { if (document.body) document.body.style.visibility = "hidden"; HMRClient.cssAutoFOUC = true; } promise.then(this.processPendingCSSImports).finally(() => { if (HMRClient.cssAutoFOUC) { // "delete" doesn't work here. Not sure why. if (document.body) { // Force layout window.getComputedStyle(document.body); document.body.style.visibility = "visible"; } HMRClient.cssAutoFOUC = false; } this.cssState = CSSImportState.Loaded; }); break; } case CSSImportState.Loaded: { promise.then( () => {}, () => {} ); break; } case CSSImportState.Loading: { this.cssQueue.push(promise); break; } } } static allImportedStyles = new Set(); static onCSSImport(event) { HMRClient.allImportedStyles.add(event.detail); if (globalThis["Bun_disableCSSImports"]) { return; } const url = event.detail; if (typeof url !== "string" || url.length === 0) { console.warn("[CSS Importer] Received invalid CSS import", url); return; } const thisURL = new URL(url, location.origin); for (let i = 0; i < document.styleSheets.length; i++) { const sheet = document.styleSheets[i]; if (!sheet.href) continue; if (sheet.href === url) { // Already imported return; } try { const _url1 = new URL(sheet.href, location.origin); if (_url1.pathname === thisURL.pathname) { // Already imported return; } } catch (e) {} } const urlString = thisURL.toString(); HMRClient.importCSS( new Promise((resolve, reject) => { if (globalThis["Bun_disableCSSImports"]) { return; } var link = document.createElement("link"); link.rel = "stylesheet"; link.href = urlString; link.onload = () => { resolve(); }; link.onerror = (evt) => { console.error( `[CSS Importer] Error loading CSS file: ${urlString}\n`, evt.toString() ); reject(); }; document.head.appendChild(link); }).then(() => Promise.resolve()) ); } static activate(verbose: boolean = false) { // Support browser-like envirnments where location and WebSocket exist // Maybe it'll work in Deno! Who knows. if ( this.client || !("location" in globalThis) || !("WebSocket" in globalThis) ) { return; } this.client = new HMRClient(); // if ( // "sessionStorage" in globalThis && // globalThis.sessionStorage.getItem("bun-hmr-session-id") // ) { // this.client.sessionId = parseInt( // globalThis.sessionStorage.getItem("bun-hmr-session-id"), // 16 // ); // } else { // this.client.sessionId = Math.floor(Math.random() * 65534); // if ("sessionStorage" in globalThis) { // try { // globalThis.sessionStorage.setItem( // "bun-hmr-session-id", // this.client.sessionId.toString(16) // ); // } catch (exception) {} // } // } this.client.verbose = verbose; this.client.start(); globalThis["__BUN_HMR"] = this.client; } handleBuildFailure(buffer: ByteBuffer, timestamp: number) { const build = API.decodeWebsocketMessageBuildFailure(buffer); const id = build.id; // const index = this.indexOfModuleId(id); // // Ignore build failures of modules that are not loaded // if (index === -1) { // this.maybeReportBuildFailure(build); // return; // } // // Build failed for a module we didn't request? // const minTimestamp = this.builds.get(index); // if (!minTimestamp) { // return; // } // const fail = API.decodeWebsocketMessageBuildFailure(buffer); this.reportBuildFailure(build); } maybeReportBuildFailure(failure: API.WebsocketMessageBuildFailure) { BunError.render(failure, this.cwd); } needsConsoleClear = false; reportBuildFailure(failure: API.WebsocketMessageBuildFailure) { BunError.render(failure, this.cwd); console.group( `Build failed: ${failure.module_path} (${failure.log.errors} errors)` ); this.needsConsoleClear = true; for (let msg of failure.log.msgs) { var logFunction; switch (msg.level) { case API.MessageLevel.err: { logFunction = console.error; break; } case API.MessageLevel.warn: { logFunction = console.warn; break; } default: { logFunction = console.info; break; } } const { text, location } = msg.data; var output = ""; if (location) { if (location.line > -1 && location.column > -1) { output += `${location.file}:${location.line}:${location.column}`; } else if (location.line > -1) { output += `${location.file}:${location.line}`; } else if (location.file.length > 0) { output += `${location.file}`; } } if (location && location.line_text) { output = output.trimRight() + "\n" + location.line_text.trim(); } output = output.trimRight() + "\n " + text; logFunction(output.trim()); } console.groupEnd(); } verbose = false; handleError = (error: ErrorEvent) => { __hmrlog.error("Websocket error", error.error); if (this.reconnect !== 0) { return; } this.debouncedReconnect(); }; handleBuildSuccess(buffer: ByteBuffer, timestamp: number) { const build = API.decodeWebsocketMessageBuildSuccess(buffer); // Ignore builds of modules we expect a later version of const currentVersion = this.builds.get(build.id) || -Infinity; if (currentVersion > build.from_timestamp) { if (this.verbose) { __hmrlog.debug( `Ignoring outdated update for "${build.module_path}".\n Expected: >=`, currentVersion, `\n Received:`, build.from_timestamp ); } return; } if (build.loader === API.Loader.css) { BunError.clear(); if (this.needsConsoleClear) { console.clear(); this.needsConsoleClear = false; } return this.loaders.css.handleBuildSuccess(buffer, build, timestamp); } const id = build.id; const index = this.indexOfModuleId(id); // Ignore builds of modules that are not loaded if (index === -1) { if (this.verbose) { __hmrlog.debug(`Skipping reload for unknown module id:`, id); } return; } if (this.verbose) { var filepath = HMRModule.dependencies.modules[index].file_path; if (filepath.startsWith(this.cwd)) { filepath = filepath.substring(this.cwd.length); } __hmrlog.debug("Preparing to reload", filepath); } var reload = new HotReload( build.id, index, build, // These are the bytes!! buffer.data.length > buffer.index ? buffer.data.subarray(buffer.index) : new Uint8Array(0), ReloadBehavior.hotReload ); reload.timings.notify = timestamp - build.from_timestamp; BunError.clear(); reload.run().then( ([module, timings]) => { var filepath = module.file_path; if (filepath.startsWith(this.cwd)) { filepath = filepath.substring(this.cwd.length); } if (this.needsConsoleClear) { console.clear(); this.needsConsoleClear = false; } __hmrlog.log( `[${formatDuration(timings.total)}ms] Reloaded`, filepath ); }, (err) => { if ( typeof err === "object" && err && err instanceof ThrottleModuleUpdateError ) { return; } __hmrlog.error("Hot Module Reload failed!", err); debugger; } ); } performFullReload() { if (typeof location !== "undefined") { if (this.socket.readyState === this.socket.OPEN) { // Disable reconnecting this.reconnect = 1; this.socket.close(); } location.reload(); } } handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) { const notification = API.decodeWebsocketMessageFileChangeNotification(buffer); let file_path = ""; switch (notification.loader) { case API.Loader.css: { file_path = this.loaders.css.filePath(notification); break; } default: { const index = HMRModule.dependencies.graph.indexOf(notification.id); if (index > -1) { file_path = HMRModule.dependencies.modules[index].file_path; } break; } } const accept = file_path && file_path.length > 0; if (!accept) { if (this.verbose) { __hmrlog.debug("Unknown module changed, skipping"); } return; } if ((this.builds.get(notification.id) || -Infinity) > timestamp) { __hmrlog.debug(`Received stale update for ${file_path}`); return; } let reloadBehavior = ReloadBehavior.ignore; switch (notification.loader) { // CSS always supports hot reloading case API.Loader.css: { this.builds.set(notification.id, timestamp); // When we're dealing with CSS, even though the watch event happened for a file in the bundle // We want it to regenerate the entire bundle // So we must swap out the ID we send for the ID of the corresponding bundle. notification.id = this.loaders.css.bundleId(); this.builds.set(notification.id, timestamp); reloadBehavior = ReloadBehavior.hotReload; break; } // The backend will detect if they have react-refresh in their bundle // If so, it will use it. // Else, it will fall back to live reloading. case API.Loader.js: case API.Loader.jsx: case API.Loader.json: case API.Loader.ts: case API.Loader.tsx: { switch (this.javascriptReloader) { case API.Reloader.disable: { break; } case API.Reloader.fast_refresh: { this.builds.set(notification.id, timestamp); reloadBehavior = ReloadBehavior.hotReload; break; } case API.Reloader.live: { reloadBehavior = ReloadBehavior.fullReload; break; } } break; } } switch (reloadBehavior) { // This is the same command/logic for both JS and CSS hot reloading. case ReloadBehavior.hotReload: { this.buildCommandBuf[0] = API.WebsocketCommandKind.build; this.buildCommandUArray[0] = timestamp; this.buildCommandBuf.set(this.buildCommandUArrayEight, 1); this.buildCommandUArray[0] = notification.id; this.buildCommandBuf.set(this.buildCommandUArrayEight, 5); this.socket.send(this.buildCommandBuf); if (this.verbose) { __hmrlog.debug(`Requesting update for ${file_path}`); } break; } case ReloadBehavior.fullReload: { this.performFullReload(); break; } } } buildCommandBuf = new Uint8Array(9); buildCommandUArray = new Uint32Array(1); buildCommandUArrayEight = new Uint8Array(this.buildCommandUArray.buffer); // On open, reset the delay for reconnecting handleOpen = (event: Event) => { globalThis.clearTimeout(this.nextReconnectAttempt); setTimeout(() => { if (this.socket && this.socket.readyState == this.socket.OPEN) { globalThis.clearTimeout(this.nextReconnectAttempt); this.reconnectDelay = 16; } }, 16); }; handleMessage = (event: MessageEvent) => { const data = new Uint8Array(event.data); const message_header_byte_buffer = new ByteBuffer(data); const header = API.decodeWebsocketMessage(message_header_byte_buffer); const buffer = new ByteBuffer( data.subarray(message_header_byte_buffer.index) ); switch (header.kind) { case API.WebsocketMessageKind.build_fail: { this.handleBuildFailure(buffer, header.timestamp); break; } case API.WebsocketMessageKind.build_success: { this.handleBuildSuccess(buffer, header.timestamp); break; } case API.WebsocketMessageKind.file_change_notification: { this.handleFileChangeNotification(buffer, header.timestamp); break; } case API.WebsocketMessageKind.welcome: { const now = performance.now(); __hmrlog.log( "HMR connected in", formatDuration(now - clientStartTime), "ms" ); clientStartTime = now; this.hasWelcomed = true; const welcome = API.decodeWebsocketMessageWelcome(buffer); this.epoch = welcome.epoch; this.javascriptReloader = welcome.javascriptReloader; this.cwd = welcome.cwd; if (!this.epoch) { __hmrlog.warn("Internal HMR error"); } break; } } }; disableReconnect = false; handleClose = (event: CloseEvent) => { if (this.reconnect !== 0 || this.disableReconnect) { return; } this.debouncedReconnect(); }; } let pendingUpdateCount = 0; class ThrottleModuleUpdateError extends Error { constructor(message) { super(message); } } class HotReload { module_id: number = 0; module_index: number = 0; build: API.WebsocketMessageBuildSuccess; timings = { notify: 0, decode: 0, import: 0, callbacks: 0, total: 0, start: 0, }; static VERBOSE = false; bytes: Uint8Array; reloader: ReloadBehavior; constructor( module_id: HotReload["module_id"], module_index: HotReload["module_index"], build: HotReload["build"], bytes: Uint8Array, reloader: ReloadBehavior ) { this.module_id = module_id; this.module_index = module_index; this.build = build; this.bytes = bytes; this.reloader = reloader; } async run() { pendingUpdateCount++; let result: [HMRModule, HotReload["timings"]]; try { result = await this._run(); } finally { pendingUpdateCount--; } return result; } private async _run(): Promise<[HMRModule, HotReload["timings"]]> { const currentPendingUpdateCount = pendingUpdateCount; const importStart = performance.now(); let orig_deps = HMRModule.dependencies; // we must preserve the updater since that holds references to the real exports. // this is a fundamental limitation of using esmodules for HMR. // we cannot export new modules. we can only mutate existing ones. const oldGraphUsed = HMRModule.dependencies.graph_used; var oldModule = HMRModule.dependencies.modules[this.module_index]; HMRModule.dependencies = orig_deps.fork(this.module_index); var blobURL = null; try { const blob = new Blob([this.bytes], { type: "text/javascript" }); blobURL = URL.createObjectURL(blob); await import(blobURL); this.timings.import = performance.now() - importStart; } catch (exception) { HMRModule.dependencies = orig_deps; URL.revokeObjectURL(blobURL); // Ensure we don't keep the bytes around longer than necessary this.bytes = null; if ("__BunRenderHMRError" in globalThis) { globalThis.__BunRenderHMRError( exception, oldModule.file_path, oldModule.id ); } oldModule = null; throw exception; } // We didn't import any new modules, so we resume as before. if (HMRModule.dependencies.graph_used === this.module_index) { HMRModule.dependencies.graph_used = oldGraphUsed; } else { // If we do import a new module, we have to do a full page reload for now } URL.revokeObjectURL(blobURL); // Ensure we don't keep the bytes around longer than necessary this.bytes = null; if (HotReload.VERBOSE) { __hmrlog.debug( "Re-imported", HMRModule.dependencies.modules[this.module_index].file_path, "in", formatDuration(this.timings.import), ". Running callbacks" ); } const callbacksStart = performance.now(); const origUpdaters = HMRModule.dependencies.modules[ this.module_index ].additional_updaters.slice(); try { switch (this.reloader) { case ReloadBehavior.hotReload: { let foundBoundary = false; if (oldModule) { HMRModule.dependencies.modules[ this.module_index ].additional_updaters.push(oldModule.update.bind(oldModule)); } const end = Math.min( this.module_index + 1, HMRModule.dependencies.graph_used ); // -- For generic hot reloading -- // ES Modules delay execution until all imports are parsed // They execute depth-first // If you load N modules and append each module ID to the array, 0 is the *last* unique module imported. // modules.length - 1 is the first. // Therefore, to reload all the modules in the correct order, we traverse the graph backwards // This only works when the graph is up to date. // If the import order changes, we need to regenerate the entire graph // Which sounds expensive, until you realize that we are mostly talking about an array that will be typically less than 1024 elements // Computers can create an array of < 1024 pointer-sized elements in < 1ms easy! // -- // -- For React Fast Refresh -- // We must find a React Refresh boundary. This is a module that only exports React components. // If we do not find a React Refresh boundary, we must instead perform a full page reload. for ( let i = 0; i <= end; i++ // let i = HMRModule.dependencies.graph_used - 1; // i > this.module_index; // i-- ) { const mod = HMRModule.dependencies.modules[i]; let handled = false; if (!mod.exports.__hmrDisable) { if (typeof mod.dispose === "function") { mod.dispose(); handled = true; } if (typeof mod.accept === "function") { mod.accept(); handled = true; } // If we don't find a boundary, we will need to do a full page load if ((mod as FastRefreshModule).isRefreshBoundary) { foundBoundary = true; } // Automatically re-initialize the dependency if (!handled) { mod.update(); } } } // By the time we get here, it's entirely possible that another update is waiting // Instead of scheduling it, we are going to just ignore this update. // But we still need to re-initialize modules regardless because otherwise a dependency may not reload properly if ( pendingUpdateCount === currentPendingUpdateCount && foundBoundary ) { FastRefreshLoader.RefreshRuntime.performReactRefresh(); } else if (pendingUpdateCount === currentPendingUpdateCount) { FastRefreshLoader.performFullRefresh(); } else { return Promise.reject( new ThrottleModuleUpdateError( `Expected pendingUpdateCount: ${currentPendingUpdateCount} but received: ${pendingUpdateCount}` ) ); } break; } } } catch (exception) { HMRModule.dependencies = orig_deps; HMRModule.dependencies.modules[this.module_index].additional_updaters = origUpdaters; throw exception; } this.timings.callbacks = performance.now() - callbacksStart; if (HotReload.VERBOSE) { __hmrlog.debug( "Ran callbacks", HMRModule.dependencies.modules[this.module_index].file_path, "in", formatDuration(this.timings.callbacks), "ms" ); } orig_deps = null; this.timings.total = this.timings.import + this.timings.callbacks + this.timings.notify; return Promise.resolve([ HMRModule.dependencies.modules[this.module_index], this.timings, ]); } } type AnyHMRModule = HMRModule | FastRefreshModule; class DependencyGraph { modules: AnyHMRModule[]; graph: Uint32Array; graph_used = 0; loadDefaults() { this.modules = new Array(32); this.graph = new Uint32Array(32); this.graph_used = 0; } static loadWithDefaults() { const graph = new DependencyGraph(); graph.loadDefaults(); return graph; } fork(offset: number) { const graph = new DependencyGraph(); graph.modules = this.modules.slice(); graph.graph_used = offset; graph.graph = this.graph.slice(); return graph; } } class HMRModule { constructor(id: number, file_path: string) { this.id = id; this.file_path = file_path; Object.defineProperty(this, "name", { get() { return this.file_path; }, configurable: false, enumerable: false, }); if (!HMRModule.dependencies) { HMRModule.dependencies = HMRModule._dependencies; } this.graph_index = HMRModule.dependencies.graph_used++; // Grow the dependencies graph if (HMRModule.dependencies.graph.length <= this.graph_index) { const new_graph = new Uint32Array( HMRModule.dependencies.graph.length * 4 ); new_graph.set(HMRModule.dependencies.graph); HMRModule.dependencies.graph = new_graph; // In-place grow. This creates a holey array, which is bad, but less bad than pushing potentially 1000 times HMRModule.dependencies.modules.length = new_graph.length; } HMRModule.dependencies.modules[this.graph_index] = this; HMRModule.dependencies.graph[this.graph_index] = this.id; } additional_files = []; additional_updaters = []; _update: (exports: Object) => void; update() { for (let update of this.additional_updaters) { update(this.exports); } this._update(this.exports); } static _dependencies = DependencyGraph.loadWithDefaults(); exportAll(object: Object) { // object[alias] must be a function for (let alias in object) { this._exports[alias] = object[alias]; Object.defineProperty(this.exports, alias, { get: this._exports[alias], configurable: true, enumerable: true, }); } } static dependencies: DependencyGraph; file_path: string; _load = function () {}; id = 0; graph_index = 0; _exports = {}; exports = {}; } function injectFastRefresh(RefreshRuntime) { if (!FastRefreshLoader.hasInjectedFastRefresh) { RefreshRuntime.injectIntoGlobalHook(globalThis); FastRefreshLoader.hasInjectedFastRefresh = true; } } class FastRefreshModule extends HMRModule { constructor(id: number, file_path: string, RefreshRuntime: any) { super(id, file_path); // 4,000,000,000 in base36 occupies 7 characters // file path is probably longer // small strings are better strings this.refreshRuntimeBaseID = (this.file_path.length > 7 ? this.id.toString(36) : this.file_path) + "/"; FastRefreshLoader.RefreshRuntime = FastRefreshLoader.RefreshRuntime || RefreshRuntime; if (!FastRefreshLoader.hasInjectedFastRefresh) { RefreshRuntime.injectIntoGlobalHook(globalThis); FastRefreshLoader.hasInjectedFastRefresh = true; } } refreshRuntimeBaseID: string; isRefreshBoundary = false; // $RefreshReg$ $r_(Component: any, id: string) { FastRefreshLoader.RefreshRuntime.register( Component, this.refreshRuntimeBaseID + id ); } // $RefreshReg$(Component, Component.name || Component.displayName) $r(Component: any) { if (!FastRefreshLoader.RefreshRuntime.isLikelyComponentType(Component)) { return; } this.$r_(Component, Component.name || Component.displayName); } // Auto-register exported React components so we only have to manually register the non-exported ones // This is what Metro does: https://github.com/facebook/metro/blob/9f2b1210a0f66378dd93e5fcaabc464c86c9e236/packages/metro-runtime/src/polyfills/require.js#L905 exportAll(object: any) { super.exportAll(object); // One thing I'm unsure of: // Do we need to register the exports object iself? Is it important for some namespacing thing? // Metro seems to do that. However, that might be an artifact of CommonJS modules. People do module.exports = SomeReactComponent. var hasExports = false; var onlyExportsComponents = true; for (const key in object) { if (key === "__esModule") { continue; } hasExports = true; // Everything in here should always be a function // exportAll({blah: () => blah}) // If you see exception right here, please file an issue and include the source file in the issue. const Component = object[key](); // Ensure exported React components always have names // This is for simpler debugging if ( Component && typeof Component === "function" && !("name" in Component) && Object.isExtensible(Component) ) { const named = { get() { return key; }, enumerable: false, configurable: true, }; // Ignore any errors if it turns out this was already set as not configurable try { // "name" is the official JavaScript way // "displayName" is the legacy React way Object.defineProperties(Component, { name: named, displayName: named, }); } catch (exception) {} } if ( !FastRefreshLoader.RefreshRuntime.isLikelyComponentType(Component) ) { onlyExportsComponents = false; // We can't stop here because we may have other exports which are components that need to be registered. continue; } this.$r_(Component, key); } this.isRefreshBoundary = hasExports && onlyExportsComponents; } loaded(_onUpdate) { this._update = _onUpdate; } } var __hmrlog = { debug(...args) { // console.debug("[bun]", ...args); console.debug(...args); }, error(...args) { // console.error("[bun]", ...args); console.error(...args); }, log(...args) { // console.log("[bun]", ...args); console.log(...args); }, warn(...args) { // console.warn("[bun]", ...args); console.warn(...args); }, }; // __HMRModule = FastRefreshModule; __HMRModule = HMRModule; __FastRefreshModule = FastRefreshModule; __HMRClient = HMRClient; __injectFastRefresh = injectFastRefresh; if ("document" in globalThis) { document.addEventListener("onimportcss", HMRClient.onCSSImport, { passive: true, }); // window.addEventListener("error", HMRClient.onError, { passive: true }); } globalThis["__BUN"] = HMRClient; } export { __HMRModule, __FastRefreshModule, __HMRClient, __injectFastRefresh };