script.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. "client";
  2. "use strict";
  3. Object.defineProperty(exports, "__esModule", {
  4. value: true
  5. });
  6. exports.handleClientScriptLoad = handleClientScriptLoad;
  7. exports.initScriptLoader = initScriptLoader;
  8. exports.default = void 0;
  9. var _extends = require("@swc/helpers/lib/_extends.js").default;
  10. var _interop_require_wildcard = require("@swc/helpers/lib/_interop_require_wildcard.js").default;
  11. var _object_without_properties_loose = require("@swc/helpers/lib/_object_without_properties_loose.js").default;
  12. var _react = _interop_require_wildcard(require("react"));
  13. var _headManagerContext = require("../shared/lib/head-manager-context");
  14. var _headManager = require("./head-manager");
  15. var _requestIdleCallback = require("./request-idle-callback");
  16. 'client';
  17. const ScriptCache = new Map();
  18. const LoadCache = new Set();
  19. const ignoreProps = [
  20. 'onLoad',
  21. 'onReady',
  22. 'dangerouslySetInnerHTML',
  23. 'children',
  24. 'onError',
  25. 'strategy',
  26. ];
  27. const loadScript = (props)=>{
  28. const { src , id , onLoad =()=>{} , onReady =null , dangerouslySetInnerHTML , children ='' , strategy ='afterInteractive' , onError , } = props;
  29. const cacheKey = id || src;
  30. // Script has already loaded
  31. if (cacheKey && LoadCache.has(cacheKey)) {
  32. return;
  33. }
  34. // Contents of this script are already loading/loaded
  35. if (ScriptCache.has(src)) {
  36. LoadCache.add(cacheKey);
  37. // It is possible that multiple `next/script` components all have same "src", but has different "onLoad"
  38. // This is to make sure the same remote script will only load once, but "onLoad" are executed in order
  39. ScriptCache.get(src).then(onLoad, onError);
  40. return;
  41. }
  42. /** Execute after the script first loaded */ const afterLoad = ()=>{
  43. // Run onReady for the first time after load event
  44. if (onReady) {
  45. onReady();
  46. }
  47. // add cacheKey to LoadCache when load successfully
  48. LoadCache.add(cacheKey);
  49. };
  50. const el = document.createElement('script');
  51. const loadPromise = new Promise((resolve, reject)=>{
  52. el.addEventListener('load', function(e) {
  53. resolve();
  54. if (onLoad) {
  55. onLoad.call(this, e);
  56. }
  57. afterLoad();
  58. });
  59. el.addEventListener('error', function(e) {
  60. reject(e);
  61. });
  62. }).catch(function(e) {
  63. if (onError) {
  64. onError(e);
  65. }
  66. });
  67. if (dangerouslySetInnerHTML) {
  68. el.innerHTML = dangerouslySetInnerHTML.__html || '';
  69. afterLoad();
  70. } else if (children) {
  71. el.textContent = typeof children === 'string' ? children : Array.isArray(children) ? children.join('') : '';
  72. afterLoad();
  73. } else if (src) {
  74. el.src = src;
  75. // do not add cacheKey into LoadCache for remote script here
  76. // cacheKey will be added to LoadCache when it is actually loaded (see loadPromise above)
  77. ScriptCache.set(src, loadPromise);
  78. }
  79. for (const [k, value] of Object.entries(props)){
  80. if (value === undefined || ignoreProps.includes(k)) {
  81. continue;
  82. }
  83. const attr = _headManager.DOMAttributeNames[k] || k.toLowerCase();
  84. el.setAttribute(attr, value);
  85. }
  86. if (strategy === 'worker') {
  87. el.setAttribute('type', 'text/partytown');
  88. }
  89. el.setAttribute('data-nscript', strategy);
  90. document.body.appendChild(el);
  91. };
  92. function handleClientScriptLoad(props) {
  93. const { strategy ='afterInteractive' } = props;
  94. if (strategy === 'lazyOnload') {
  95. window.addEventListener('load', ()=>{
  96. (0, _requestIdleCallback).requestIdleCallback(()=>loadScript(props));
  97. });
  98. } else {
  99. loadScript(props);
  100. }
  101. }
  102. function loadLazyScript(props) {
  103. if (document.readyState === 'complete') {
  104. (0, _requestIdleCallback).requestIdleCallback(()=>loadScript(props));
  105. } else {
  106. window.addEventListener('load', ()=>{
  107. (0, _requestIdleCallback).requestIdleCallback(()=>loadScript(props));
  108. });
  109. }
  110. }
  111. function addBeforeInteractiveToCache() {
  112. const scripts = [
  113. ...document.querySelectorAll('[data-nscript="beforeInteractive"]'),
  114. ...document.querySelectorAll('[data-nscript="beforePageRender"]'),
  115. ];
  116. scripts.forEach((script)=>{
  117. const cacheKey = script.id || script.getAttribute('src');
  118. LoadCache.add(cacheKey);
  119. });
  120. }
  121. function initScriptLoader(scriptLoaderItems) {
  122. scriptLoaderItems.forEach(handleClientScriptLoad);
  123. addBeforeInteractiveToCache();
  124. }
  125. function Script(props) {
  126. const { id , src ='' , onLoad =()=>{} , onReady =null , strategy ='afterInteractive' , onError } = props, restProps = _object_without_properties_loose(props, [
  127. "id",
  128. "src",
  129. "onLoad",
  130. "onReady",
  131. "strategy",
  132. "onError"
  133. ]);
  134. // Context is available only during SSR
  135. const { updateScripts , scripts , getIsSsr } = (0, _react).useContext(_headManagerContext.HeadManagerContext);
  136. /**
  137. * - First mount:
  138. * 1. The useEffect for onReady executes
  139. * 2. hasOnReadyEffectCalled.current is false, but the script hasn't loaded yet (not in LoadCache)
  140. * onReady is skipped, set hasOnReadyEffectCalled.current to true
  141. * 3. The useEffect for loadScript executes
  142. * 4. hasLoadScriptEffectCalled.current is false, loadScript executes
  143. * Once the script is loaded, the onLoad and onReady will be called by then
  144. * [If strict mode is enabled / is wrapped in <OffScreen /> component]
  145. * 5. The useEffect for onReady executes again
  146. * 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped
  147. * 7. The useEffect for loadScript executes again
  148. * 8. hasLoadScriptEffectCalled.current is true, so entire effect is skipped
  149. *
  150. * - Second mount:
  151. * 1. The useEffect for onReady executes
  152. * 2. hasOnReadyEffectCalled.current is false, but the script has already loaded (found in LoadCache)
  153. * onReady is called, set hasOnReadyEffectCalled.current to true
  154. * 3. The useEffect for loadScript executes
  155. * 4. The script is already loaded, loadScript bails out
  156. * [If strict mode is enabled / is wrapped in <OffScreen /> component]
  157. * 5. The useEffect for onReady executes again
  158. * 6. hasOnReadyEffectCalled.current is true, so entire effect is skipped
  159. * 7. The useEffect for loadScript executes again
  160. * 8. hasLoadScriptEffectCalled.current is true, so entire effect is skipped
  161. */ const hasOnReadyEffectCalled = (0, _react).useRef(false);
  162. (0, _react).useEffect(()=>{
  163. const cacheKey = id || src;
  164. if (!hasOnReadyEffectCalled.current) {
  165. // Run onReady if script has loaded before but component is re-mounted
  166. if (onReady && cacheKey && LoadCache.has(cacheKey)) {
  167. onReady();
  168. }
  169. hasOnReadyEffectCalled.current = true;
  170. }
  171. }, [
  172. onReady,
  173. id,
  174. src
  175. ]);
  176. const hasLoadScriptEffectCalled = (0, _react).useRef(false);
  177. (0, _react).useEffect(()=>{
  178. if (!hasLoadScriptEffectCalled.current) {
  179. if (strategy === 'afterInteractive') {
  180. loadScript(props);
  181. } else if (strategy === 'lazyOnload') {
  182. loadLazyScript(props);
  183. }
  184. hasLoadScriptEffectCalled.current = true;
  185. }
  186. }, [
  187. props,
  188. strategy
  189. ]);
  190. if (strategy === 'beforeInteractive' || strategy === 'worker') {
  191. if (updateScripts) {
  192. scripts[strategy] = (scripts[strategy] || []).concat([
  193. _extends({
  194. id,
  195. src,
  196. onLoad,
  197. onReady,
  198. onError
  199. }, restProps),
  200. ]);
  201. updateScripts(scripts);
  202. } else if (getIsSsr && getIsSsr()) {
  203. // Script has already loaded during SSR
  204. LoadCache.add(id || src);
  205. } else if (getIsSsr && !getIsSsr()) {
  206. loadScript(props);
  207. }
  208. }
  209. return null;
  210. }
  211. Object.defineProperty(Script, '__nextScript', {
  212. value: true
  213. });
  214. var _default = Script;
  215. exports.default = _default;
  216. if ((typeof exports.default === 'function' || (typeof exports.default === 'object' && exports.default !== null)) && typeof exports.default.__esModule === 'undefined') {
  217. Object.defineProperty(exports.default, '__esModule', { value: true });
  218. Object.assign(exports.default, exports);
  219. module.exports = exports.default;
  220. }
  221. //# sourceMappingURL=script.js.map