refresh-runtime.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. /* global window */
  2. /* eslint-disable eqeqeq, prefer-const, @typescript-eslint/no-empty-function */
  3. /*! Copyright (c) Meta Platforms, Inc. and affiliates. **/
  4. /**
  5. * This is simplified pure-js version of https://github.com/facebook/react/blob/main/packages/react-refresh/src/ReactFreshRuntime.js
  6. * without IE11 compatibility and verbose isDev checks.
  7. * Some utils are appended at the bottom for HMR integration.
  8. */
  9. const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref')
  10. const REACT_MEMO_TYPE = Symbol.for('react.memo')
  11. // We never remove these associations.
  12. // It's OK to reference families, but use WeakMap/Set for types.
  13. let allFamiliesByID = new Map()
  14. let allFamiliesByType = new WeakMap()
  15. let allSignaturesByType = new WeakMap()
  16. // This WeakMap is read by React, so we only put families
  17. // that have actually been edited here. This keeps checks fast.
  18. const updatedFamiliesByType = new WeakMap()
  19. // This is cleared on every performReactRefresh() call.
  20. // It is an array of [Family, NextType] tuples.
  21. let pendingUpdates = []
  22. // This is injected by the renderer via DevTools global hook.
  23. const helpersByRendererID = new Map()
  24. const helpersByRoot = new Map()
  25. // We keep track of mounted roots so we can schedule updates.
  26. const mountedRoots = new Set()
  27. // If a root captures an error, we remember it so we can retry on edit.
  28. const failedRoots = new Set()
  29. // We also remember the last element for every root.
  30. // It needs to be weak because we do this even for roots that failed to mount.
  31. // If there is no WeakMap, we won't attempt to do retrying.
  32. let rootElements = new WeakMap()
  33. let isPerformingRefresh = false
  34. function computeFullKey(signature) {
  35. if (signature.fullKey !== null) {
  36. return signature.fullKey
  37. }
  38. let fullKey = signature.ownKey
  39. let hooks
  40. try {
  41. hooks = signature.getCustomHooks()
  42. } catch (err) {
  43. // This can happen in an edge case, e.g. if expression like Foo.useSomething
  44. // depends on Foo which is lazily initialized during rendering.
  45. // In that case just assume we'll have to remount.
  46. signature.forceReset = true
  47. signature.fullKey = fullKey
  48. return fullKey
  49. }
  50. for (let i = 0; i < hooks.length; i++) {
  51. const hook = hooks[i]
  52. if (typeof hook !== 'function') {
  53. // Something's wrong. Assume we need to remount.
  54. signature.forceReset = true
  55. signature.fullKey = fullKey
  56. return fullKey
  57. }
  58. const nestedHookSignature = allSignaturesByType.get(hook)
  59. if (nestedHookSignature === undefined) {
  60. // No signature means Hook wasn't in the source code, e.g. in a library.
  61. // We'll skip it because we can assume it won't change during this session.
  62. continue
  63. }
  64. const nestedHookKey = computeFullKey(nestedHookSignature)
  65. if (nestedHookSignature.forceReset) {
  66. signature.forceReset = true
  67. }
  68. fullKey += '\n---\n' + nestedHookKey
  69. }
  70. signature.fullKey = fullKey
  71. return fullKey
  72. }
  73. function haveEqualSignatures(prevType, nextType) {
  74. const prevSignature = allSignaturesByType.get(prevType)
  75. const nextSignature = allSignaturesByType.get(nextType)
  76. if (prevSignature === undefined && nextSignature === undefined) {
  77. return true
  78. }
  79. if (prevSignature === undefined || nextSignature === undefined) {
  80. return false
  81. }
  82. if (computeFullKey(prevSignature) !== computeFullKey(nextSignature)) {
  83. return false
  84. }
  85. if (nextSignature.forceReset) {
  86. return false
  87. }
  88. return true
  89. }
  90. function isReactClass(type) {
  91. return type.prototype && type.prototype.isReactComponent
  92. }
  93. function canPreserveStateBetween(prevType, nextType) {
  94. if (isReactClass(prevType) || isReactClass(nextType)) {
  95. return false
  96. }
  97. if (haveEqualSignatures(prevType, nextType)) {
  98. return true
  99. }
  100. return false
  101. }
  102. function resolveFamily(type) {
  103. // Only check updated types to keep lookups fast.
  104. return updatedFamiliesByType.get(type)
  105. }
  106. // This is a safety mechanism to protect against rogue getters and Proxies.
  107. function getProperty(object, property) {
  108. try {
  109. return object[property]
  110. } catch (err) {
  111. // Intentionally ignore.
  112. return undefined
  113. }
  114. }
  115. function performReactRefresh() {
  116. if (pendingUpdates.length === 0) {
  117. return null
  118. }
  119. if (isPerformingRefresh) {
  120. return null
  121. }
  122. isPerformingRefresh = true
  123. try {
  124. const staleFamilies = new Set()
  125. const updatedFamilies = new Set()
  126. const updates = pendingUpdates
  127. pendingUpdates = []
  128. updates.forEach(([family, nextType]) => {
  129. // Now that we got a real edit, we can create associations
  130. // that will be read by the React reconciler.
  131. const prevType = family.current
  132. updatedFamiliesByType.set(prevType, family)
  133. updatedFamiliesByType.set(nextType, family)
  134. family.current = nextType
  135. // Determine whether this should be a re-render or a re-mount.
  136. if (canPreserveStateBetween(prevType, nextType)) {
  137. updatedFamilies.add(family)
  138. } else {
  139. staleFamilies.add(family)
  140. }
  141. })
  142. // TODO: rename these fields to something more meaningful.
  143. const update = {
  144. updatedFamilies, // Families that will re-render preserving state
  145. staleFamilies, // Families that will be remounted
  146. }
  147. helpersByRendererID.forEach((helpers) => {
  148. // Even if there are no roots, set the handler on first update.
  149. // This ensures that if *new* roots are mounted, they'll use the resolve handler.
  150. helpers.setRefreshHandler(resolveFamily)
  151. })
  152. let didError = false
  153. let firstError = null
  154. // We snapshot maps and sets that are mutated during commits.
  155. // If we don't do this, there is a risk they will be mutated while
  156. // we iterate over them. For example, trying to recover a failed root
  157. // may cause another root to be added to the failed list -- an infinite loop.
  158. const failedRootsSnapshot = new Set(failedRoots)
  159. const mountedRootsSnapshot = new Set(mountedRoots)
  160. const helpersByRootSnapshot = new Map(helpersByRoot)
  161. failedRootsSnapshot.forEach((root) => {
  162. const helpers = helpersByRootSnapshot.get(root)
  163. if (helpers === undefined) {
  164. throw new Error(
  165. 'Could not find helpers for a root. This is a bug in React Refresh.',
  166. )
  167. }
  168. if (!failedRoots.has(root)) {
  169. // No longer failed.
  170. }
  171. if (rootElements === null) {
  172. return
  173. }
  174. if (!rootElements.has(root)) {
  175. return
  176. }
  177. const element = rootElements.get(root)
  178. try {
  179. helpers.scheduleRoot(root, element)
  180. } catch (err) {
  181. if (!didError) {
  182. didError = true
  183. firstError = err
  184. }
  185. // Keep trying other roots.
  186. }
  187. })
  188. mountedRootsSnapshot.forEach((root) => {
  189. const helpers = helpersByRootSnapshot.get(root)
  190. if (helpers === undefined) {
  191. throw new Error(
  192. 'Could not find helpers for a root. This is a bug in React Refresh.',
  193. )
  194. }
  195. if (!mountedRoots.has(root)) {
  196. // No longer mounted.
  197. }
  198. try {
  199. helpers.scheduleRefresh(root, update)
  200. } catch (err) {
  201. if (!didError) {
  202. didError = true
  203. firstError = err
  204. }
  205. // Keep trying other roots.
  206. }
  207. })
  208. if (didError) {
  209. throw firstError
  210. }
  211. return update
  212. } finally {
  213. isPerformingRefresh = false
  214. }
  215. }
  216. function register(type, id) {
  217. if (type === null) {
  218. return
  219. }
  220. if (typeof type !== 'function' && typeof type !== 'object') {
  221. return
  222. }
  223. // This can happen in an edge case, e.g. if we register
  224. // return value of a HOC but it returns a cached component.
  225. // Ignore anything but the first registration for each type.
  226. if (allFamiliesByType.has(type)) {
  227. return
  228. }
  229. // Create family or remember to update it.
  230. // None of this bookkeeping affects reconciliation
  231. // until the first performReactRefresh() call above.
  232. let family = allFamiliesByID.get(id)
  233. if (family === undefined) {
  234. family = { current: type }
  235. allFamiliesByID.set(id, family)
  236. } else {
  237. pendingUpdates.push([family, type])
  238. }
  239. allFamiliesByType.set(type, family)
  240. // Visit inner types because we might not have registered them.
  241. if (typeof type === 'object' && type !== null) {
  242. switch (getProperty(type, '$$typeof')) {
  243. case REACT_FORWARD_REF_TYPE:
  244. register(type.render, id + '$render')
  245. break
  246. case REACT_MEMO_TYPE:
  247. register(type.type, id + '$type')
  248. break
  249. }
  250. }
  251. }
  252. function setSignature(type, key, forceReset, getCustomHooks) {
  253. if (!allSignaturesByType.has(type)) {
  254. allSignaturesByType.set(type, {
  255. forceReset,
  256. ownKey: key,
  257. fullKey: null,
  258. getCustomHooks: getCustomHooks || (() => []),
  259. })
  260. }
  261. // Visit inner types because we might not have signed them.
  262. if (typeof type === 'object' && type !== null) {
  263. switch (getProperty(type, '$$typeof')) {
  264. case REACT_FORWARD_REF_TYPE:
  265. setSignature(type.render, key, forceReset, getCustomHooks)
  266. break
  267. case REACT_MEMO_TYPE:
  268. setSignature(type.type, key, forceReset, getCustomHooks)
  269. break
  270. }
  271. }
  272. }
  273. // This is lazily called during first render for a type.
  274. // It captures Hook list at that time so inline requires don't break comparisons.
  275. function collectCustomHooksForSignature(type) {
  276. const signature = allSignaturesByType.get(type)
  277. if (signature !== undefined) {
  278. computeFullKey(signature)
  279. }
  280. }
  281. export function injectIntoGlobalHook(globalObject) {
  282. // For React Native, the global hook will be set up by require('react-devtools-core').
  283. // That code will run before us. So we need to monkeypatch functions on existing hook.
  284. // For React Web, the global hook will be set up by the extension.
  285. // This will also run before us.
  286. let hook = globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__
  287. if (hook === undefined) {
  288. // However, if there is no DevTools extension, we'll need to set up the global hook ourselves.
  289. // Note that in this case it's important that renderer code runs *after* this method call.
  290. // Otherwise, the renderer will think that there is no global hook, and won't do the injection.
  291. let nextID = 0
  292. globalObject.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook = {
  293. renderers: new Map(),
  294. supportsFiber: true,
  295. inject: (injected) => nextID++,
  296. onScheduleFiberRoot: (id, root, children) => {},
  297. onCommitFiberRoot: (id, root, maybePriorityLevel, didError) => {},
  298. onCommitFiberUnmount() {},
  299. }
  300. }
  301. if (hook.isDisabled) {
  302. // This isn't a real property on the hook, but it can be set to opt out
  303. // of DevTools integration and associated warnings and logs.
  304. // Using console['warn'] to evade Babel and ESLint
  305. console['warn'](
  306. 'Something has shimmed the React DevTools global hook (__REACT_DEVTOOLS_GLOBAL_HOOK__). ' +
  307. 'Fast Refresh is not compatible with this shim and will be disabled.',
  308. )
  309. return
  310. }
  311. // Here, we just want to get a reference to scheduleRefresh.
  312. const oldInject = hook.inject
  313. hook.inject = function (injected) {
  314. const id = oldInject.apply(this, arguments)
  315. if (
  316. typeof injected.scheduleRefresh === 'function' &&
  317. typeof injected.setRefreshHandler === 'function'
  318. ) {
  319. // This version supports React Refresh.
  320. helpersByRendererID.set(id, injected)
  321. }
  322. return id
  323. }
  324. // Do the same for any already injected roots.
  325. // This is useful if ReactDOM has already been initialized.
  326. // https://github.com/facebook/react/issues/17626
  327. hook.renderers.forEach((injected, id) => {
  328. if (
  329. typeof injected.scheduleRefresh === 'function' &&
  330. typeof injected.setRefreshHandler === 'function'
  331. ) {
  332. // This version supports React Refresh.
  333. helpersByRendererID.set(id, injected)
  334. }
  335. })
  336. // We also want to track currently mounted roots.
  337. const oldOnCommitFiberRoot = hook.onCommitFiberRoot
  338. const oldOnScheduleFiberRoot = hook.onScheduleFiberRoot || (() => {})
  339. hook.onScheduleFiberRoot = function (id, root, children) {
  340. if (!isPerformingRefresh) {
  341. // If it was intentionally scheduled, don't attempt to restore.
  342. // This includes intentionally scheduled unmounts.
  343. failedRoots.delete(root)
  344. if (rootElements !== null) {
  345. rootElements.set(root, children)
  346. }
  347. }
  348. return oldOnScheduleFiberRoot.apply(this, arguments)
  349. }
  350. hook.onCommitFiberRoot = function (id, root, maybePriorityLevel, didError) {
  351. const helpers = helpersByRendererID.get(id)
  352. if (helpers !== undefined) {
  353. helpersByRoot.set(root, helpers)
  354. const current = root.current
  355. const alternate = current.alternate
  356. // We need to determine whether this root has just (un)mounted.
  357. // This logic is copy-pasted from similar logic in the DevTools backend.
  358. // If this breaks with some refactoring, you'll want to update DevTools too.
  359. if (alternate !== null) {
  360. const wasMounted =
  361. alternate.memoizedState != null &&
  362. alternate.memoizedState.element != null &&
  363. mountedRoots.has(root)
  364. const isMounted =
  365. current.memoizedState != null && current.memoizedState.element != null
  366. if (!wasMounted && isMounted) {
  367. // Mount a new root.
  368. mountedRoots.add(root)
  369. failedRoots.delete(root)
  370. } else if (wasMounted && isMounted) {
  371. // Update an existing root.
  372. // This doesn't affect our mounted root Set.
  373. } else if (wasMounted && !isMounted) {
  374. // Unmount an existing root.
  375. mountedRoots.delete(root)
  376. if (didError) {
  377. // We'll remount it on future edits.
  378. failedRoots.add(root)
  379. } else {
  380. helpersByRoot.delete(root)
  381. }
  382. } else if (!wasMounted && !isMounted) {
  383. if (didError) {
  384. // We'll remount it on future edits.
  385. failedRoots.add(root)
  386. }
  387. }
  388. } else {
  389. // Mount a new root.
  390. mountedRoots.add(root)
  391. }
  392. }
  393. // Always call the decorated DevTools hook.
  394. return oldOnCommitFiberRoot.apply(this, arguments)
  395. }
  396. }
  397. // This is a wrapper over more primitive functions for setting signature.
  398. // Signatures let us decide whether the Hook order has changed on refresh.
  399. //
  400. // This function is intended to be used as a transform target, e.g.:
  401. // var _s = createSignatureFunctionForTransform()
  402. //
  403. // function Hello() {
  404. // const [foo, setFoo] = useState(0);
  405. // const value = useCustomHook();
  406. // _s(); /* Call without arguments triggers collecting the custom Hook list.
  407. // * This doesn't happen during the module evaluation because we
  408. // * don't want to change the module order with inline requires.
  409. // * Next calls are noops. */
  410. // return <h1>Hi</h1>;
  411. // }
  412. //
  413. // /* Call with arguments attaches the signature to the type: */
  414. // _s(
  415. // Hello,
  416. // 'useState{[foo, setFoo]}(0)',
  417. // () => [useCustomHook], /* Lazy to avoid triggering inline requires */
  418. // );
  419. export function createSignatureFunctionForTransform() {
  420. let savedType
  421. let hasCustomHooks
  422. let didCollectHooks = false
  423. return function (type, key, forceReset, getCustomHooks) {
  424. if (typeof key === 'string') {
  425. // We're in the initial phase that associates signatures
  426. // with the functions. Note this may be called multiple times
  427. // in HOC chains like _s(hoc1(_s(hoc2(_s(actualFunction))))).
  428. if (!savedType) {
  429. // We're in the innermost call, so this is the actual type.
  430. // $FlowFixMe[escaped-generic] discovered when updating Flow
  431. savedType = type
  432. hasCustomHooks = typeof getCustomHooks === 'function'
  433. }
  434. // Set the signature for all types (even wrappers!) in case
  435. // they have no signatures of their own. This is to prevent
  436. // problems like https://github.com/facebook/react/issues/20417.
  437. if (
  438. type != null &&
  439. (typeof type === 'function' || typeof type === 'object')
  440. ) {
  441. setSignature(type, key, forceReset, getCustomHooks)
  442. }
  443. return type
  444. } else {
  445. // We're in the _s() call without arguments, which means
  446. // this is the time to collect custom Hook signatures.
  447. // Only do this once. This path is hot and runs *inside* every render!
  448. if (!didCollectHooks && hasCustomHooks) {
  449. didCollectHooks = true
  450. collectCustomHooksForSignature(savedType)
  451. }
  452. }
  453. }
  454. }
  455. function isLikelyComponentType(type) {
  456. switch (typeof type) {
  457. case 'function': {
  458. // First, deal with classes.
  459. if (type.prototype != null) {
  460. if (type.prototype.isReactComponent) {
  461. // React class.
  462. return true
  463. }
  464. const ownNames = Object.getOwnPropertyNames(type.prototype)
  465. if (ownNames.length > 1 || ownNames[0] !== 'constructor') {
  466. // This looks like a class.
  467. return false
  468. }
  469. if (type.prototype.__proto__ !== Object.prototype) {
  470. // It has a superclass.
  471. return false
  472. }
  473. // Pass through.
  474. // This looks like a regular function with empty prototype.
  475. }
  476. // For plain functions and arrows, use name as a heuristic.
  477. const name = type.name || type.displayName
  478. return typeof name === 'string' && /^[A-Z]/.test(name)
  479. }
  480. case 'object': {
  481. if (type != null) {
  482. switch (getProperty(type, '$$typeof')) {
  483. case REACT_FORWARD_REF_TYPE:
  484. case REACT_MEMO_TYPE:
  485. // Definitely React components.
  486. return true
  487. default:
  488. return false
  489. }
  490. }
  491. return false
  492. }
  493. default: {
  494. return false
  495. }
  496. }
  497. }
  498. function isCompoundComponent(type) {
  499. if (!isPlainObject(type)) return false
  500. for (const key in type) {
  501. if (!isLikelyComponentType(type[key])) return false
  502. }
  503. return true
  504. }
  505. function isPlainObject(obj) {
  506. return (
  507. Object.prototype.toString.call(obj) === '[object Object]' &&
  508. (obj.constructor === Object || obj.constructor === undefined)
  509. )
  510. }
  511. /**
  512. * Plugin utils
  513. */
  514. export function getRefreshReg(filename) {
  515. return (type, id) => register(type, filename + ' ' + id)
  516. }
  517. // Taken from https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/main/lib/runtime/RefreshUtils.js#L141
  518. // This allows to resister components not detected by SWC like styled component
  519. export function registerExportsForReactRefresh(filename, moduleExports) {
  520. for (const key in moduleExports) {
  521. if (key === '__esModule') continue
  522. const exportValue = moduleExports[key]
  523. if (isLikelyComponentType(exportValue)) {
  524. // 'export' is required to avoid key collision when renamed exports that
  525. // shadow a local component name: https://github.com/vitejs/vite-plugin-react/issues/116
  526. // The register function has an identity check to not register twice the same component,
  527. // so this is safe to not used the same key here.
  528. register(exportValue, filename + ' export ' + key)
  529. } else if (isCompoundComponent(exportValue)) {
  530. for (const subKey in exportValue) {
  531. register(
  532. exportValue[subKey],
  533. filename + ' export ' + key + '-' + subKey,
  534. )
  535. }
  536. }
  537. }
  538. }
  539. function debounce(fn, delay) {
  540. let handle
  541. return () => {
  542. clearTimeout(handle)
  543. handle = setTimeout(fn, delay)
  544. }
  545. }
  546. const hooks = []
  547. window.__registerBeforePerformReactRefresh = (cb) => {
  548. hooks.push(cb)
  549. }
  550. const enqueueUpdate = debounce(async () => {
  551. if (hooks.length) await Promise.all(hooks.map((cb) => cb()))
  552. performReactRefresh()
  553. }, 16)
  554. export function validateRefreshBoundaryAndEnqueueUpdate(
  555. id,
  556. prevExports,
  557. nextExports,
  558. ) {
  559. const ignoredExports = window.__getReactRefreshIgnoredExports?.({ id }) ?? []
  560. if (
  561. predicateOnExport(
  562. ignoredExports,
  563. prevExports,
  564. (key) => key in nextExports,
  565. ) !== true
  566. ) {
  567. return 'Could not Fast Refresh (export removed)'
  568. }
  569. if (
  570. predicateOnExport(
  571. ignoredExports,
  572. nextExports,
  573. (key) => key in prevExports,
  574. ) !== true
  575. ) {
  576. return 'Could not Fast Refresh (new export)'
  577. }
  578. let hasExports = false
  579. const allExportsAreComponentsOrUnchanged = predicateOnExport(
  580. ignoredExports,
  581. nextExports,
  582. (key, value) => {
  583. hasExports = true
  584. if (isLikelyComponentType(value)) return true
  585. if (isCompoundComponent(value)) return true
  586. return prevExports[key] === nextExports[key]
  587. },
  588. )
  589. if (hasExports && allExportsAreComponentsOrUnchanged === true) {
  590. enqueueUpdate()
  591. } else {
  592. return `Could not Fast Refresh ("${allExportsAreComponentsOrUnchanged}" export is incompatible). Learn more at __README_URL__#consistent-components-exports`
  593. }
  594. }
  595. function predicateOnExport(ignoredExports, moduleExports, predicate) {
  596. for (const key in moduleExports) {
  597. if (key === '__esModule') continue
  598. if (ignoredExports.includes(key)) continue
  599. const desc = Object.getOwnPropertyDescriptor(moduleExports, key)
  600. if (desc && desc.get) return key
  601. if (!predicate(key, moduleExports[key])) return key
  602. }
  603. return true
  604. }
  605. // Hides vite-ignored dynamic import so that Vite can skip analysis if no other
  606. // dynamic import is present (https://github.com/vitejs/vite/pull/12732)
  607. export const __hmr_import = (module) => import(/* @vite-ignore */ module)
  608. // For backwards compatibility with @vitejs/plugin-react.
  609. export default { injectIntoGlobalHook }