client.mjs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import '@vite/env';
  2. const template = /*html*/ `
  3. <style>
  4. :host {
  5. position: fixed;
  6. z-index: 99999;
  7. top: 0;
  8. left: 0;
  9. width: 100%;
  10. height: 100%;
  11. overflow-y: scroll;
  12. margin: 0;
  13. background: rgba(0, 0, 0, 0.66);
  14. --monospace: 'SFMono-Regular', Consolas,
  15. 'Liberation Mono', Menlo, Courier, monospace;
  16. --red: #ff5555;
  17. --yellow: #e2aa53;
  18. --purple: #cfa4ff;
  19. --cyan: #2dd9da;
  20. --dim: #c9c9c9;
  21. }
  22. .window {
  23. font-family: var(--monospace);
  24. line-height: 1.5;
  25. width: 800px;
  26. color: #d8d8d8;
  27. margin: 30px auto;
  28. padding: 25px 40px;
  29. position: relative;
  30. background: #181818;
  31. border-radius: 6px 6px 8px 8px;
  32. box-shadow: 0 19px 38px rgba(0,0,0,0.30), 0 15px 12px rgba(0,0,0,0.22);
  33. overflow: hidden;
  34. border-top: 8px solid var(--red);
  35. direction: ltr;
  36. text-align: left;
  37. }
  38. pre {
  39. font-family: var(--monospace);
  40. font-size: 16px;
  41. margin-top: 0;
  42. margin-bottom: 1em;
  43. overflow-x: scroll;
  44. scrollbar-width: none;
  45. }
  46. pre::-webkit-scrollbar {
  47. display: none;
  48. }
  49. .message {
  50. line-height: 1.3;
  51. font-weight: 600;
  52. white-space: pre-wrap;
  53. }
  54. .message-body {
  55. color: var(--red);
  56. }
  57. .plugin {
  58. color: var(--purple);
  59. }
  60. .file {
  61. color: var(--cyan);
  62. margin-bottom: 0;
  63. white-space: pre-wrap;
  64. word-break: break-all;
  65. }
  66. .frame {
  67. color: var(--yellow);
  68. }
  69. .stack {
  70. font-size: 13px;
  71. color: var(--dim);
  72. }
  73. .tip {
  74. font-size: 13px;
  75. color: #999;
  76. border-top: 1px dotted #999;
  77. padding-top: 13px;
  78. }
  79. code {
  80. font-size: 13px;
  81. font-family: var(--monospace);
  82. color: var(--yellow);
  83. }
  84. .file-link {
  85. text-decoration: underline;
  86. cursor: pointer;
  87. }
  88. </style>
  89. <div class="window">
  90. <pre class="message"><span class="plugin"></span><span class="message-body"></span></pre>
  91. <pre class="file"></pre>
  92. <pre class="frame"></pre>
  93. <pre class="stack"></pre>
  94. <div class="tip">
  95. Click outside or fix the code to dismiss.<br>
  96. You can also disable this overlay by setting
  97. <code>server.hmr.overlay</code> to <code>false</code> in <code>vite.config.js.</code>
  98. </div>
  99. </div>
  100. `;
  101. const fileRE = /(?:[a-zA-Z]:\\|\/).*?:\d+:\d+/g;
  102. const codeframeRE = /^(?:>?\s+\d+\s+\|.*|\s+\|\s*\^.*)\r?\n/gm;
  103. class ErrorOverlay extends HTMLElement {
  104. constructor(err) {
  105. var _a;
  106. super();
  107. this.root = this.attachShadow({ mode: 'open' });
  108. this.root.innerHTML = template;
  109. codeframeRE.lastIndex = 0;
  110. const hasFrame = err.frame && codeframeRE.test(err.frame);
  111. const message = hasFrame
  112. ? err.message.replace(codeframeRE, '')
  113. : err.message;
  114. if (err.plugin) {
  115. this.text('.plugin', `[plugin:${err.plugin}] `);
  116. }
  117. this.text('.message-body', message.trim());
  118. const [file] = (((_a = err.loc) === null || _a === void 0 ? void 0 : _a.file) || err.id || 'unknown file').split(`?`);
  119. if (err.loc) {
  120. this.text('.file', `${file}:${err.loc.line}:${err.loc.column}`, true);
  121. }
  122. else if (err.id) {
  123. this.text('.file', file);
  124. }
  125. if (hasFrame) {
  126. this.text('.frame', err.frame.trim());
  127. }
  128. this.text('.stack', err.stack, true);
  129. this.root.querySelector('.window').addEventListener('click', (e) => {
  130. e.stopPropagation();
  131. });
  132. this.addEventListener('click', () => {
  133. this.close();
  134. });
  135. }
  136. text(selector, text, linkFiles = false) {
  137. const el = this.root.querySelector(selector);
  138. if (!linkFiles) {
  139. el.textContent = text;
  140. }
  141. else {
  142. let curIndex = 0;
  143. let match;
  144. while ((match = fileRE.exec(text))) {
  145. const { 0: file, index } = match;
  146. if (index != null) {
  147. const frag = text.slice(curIndex, index);
  148. el.appendChild(document.createTextNode(frag));
  149. const link = document.createElement('a');
  150. link.textContent = file;
  151. link.className = 'file-link';
  152. link.onclick = () => {
  153. fetch('/__open-in-editor?file=' + encodeURIComponent(file));
  154. };
  155. el.appendChild(link);
  156. curIndex += frag.length + file.length;
  157. }
  158. }
  159. }
  160. }
  161. close() {
  162. var _a;
  163. (_a = this.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(this);
  164. }
  165. }
  166. const overlayId = 'vite-error-overlay';
  167. if (customElements && !customElements.get(overlayId)) {
  168. customElements.define(overlayId, ErrorOverlay);
  169. }
  170. console.log('[vite] connecting...');
  171. // use server configuration, then fallback to inference
  172. const socketProtocol = __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws');
  173. const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;
  174. const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr');
  175. const base = __BASE__ || '/';
  176. const messageBuffer = [];
  177. function warnFailedFetch(err, path) {
  178. if (!err.message.match('fetch')) {
  179. console.error(err);
  180. }
  181. console.error(`[hmr] Failed to reload ${path}. ` +
  182. `This could be due to syntax errors or importing non-existent ` +
  183. `modules. (see errors above)`);
  184. }
  185. function cleanUrl(pathname) {
  186. const url = new URL(pathname, location.toString());
  187. url.searchParams.delete('direct');
  188. return url.pathname + url.search;
  189. }
  190. // Listen for messages
  191. socket.addEventListener('message', async ({ data }) => {
  192. handleMessage(JSON.parse(data));
  193. });
  194. let isFirstUpdate = true;
  195. async function handleMessage(payload) {
  196. switch (payload.type) {
  197. case 'connected':
  198. console.log(`[vite] connected.`);
  199. sendMessageBuffer();
  200. // proxy(nginx, docker) hmr ws maybe caused timeout,
  201. // so send ping package let ws keep alive.
  202. setInterval(() => socket.send('{"type":"ping"}'), __HMR_TIMEOUT__);
  203. break;
  204. case 'update':
  205. notifyListeners('vite:beforeUpdate', payload);
  206. // if this is the first update and there's already an error overlay, it
  207. // means the page opened with existing server compile error and the whole
  208. // module script failed to load (since one of the nested imports is 500).
  209. // in this case a normal update won't work and a full reload is needed.
  210. if (isFirstUpdate && hasErrorOverlay()) {
  211. window.location.reload();
  212. return;
  213. }
  214. else {
  215. clearErrorOverlay();
  216. isFirstUpdate = false;
  217. }
  218. payload.updates.forEach((update) => {
  219. if (update.type === 'js-update') {
  220. queueUpdate(fetchUpdate(update));
  221. }
  222. else {
  223. // css-update
  224. // this is only sent when a css file referenced with <link> is updated
  225. const { path, timestamp } = update;
  226. const searchUrl = cleanUrl(path);
  227. // can't use querySelector with `[href*=]` here since the link may be
  228. // using relative paths so we need to use link.href to grab the full
  229. // URL for the include check.
  230. const el = Array.from(document.querySelectorAll('link')).find((e) => cleanUrl(e.href).includes(searchUrl));
  231. if (el) {
  232. const newPath = `${base}${searchUrl.slice(1)}${searchUrl.includes('?') ? '&' : '?'}t=${timestamp}`;
  233. // rather than swapping the href on the existing tag, we will
  234. // create a new link tag. Once the new stylesheet has loaded we
  235. // will remove the existing link tag. This removes a Flash Of
  236. // Unstyled Content that can occur when swapping out the tag href
  237. // directly, as the new stylesheet has not yet been loaded.
  238. const newLinkTag = el.cloneNode();
  239. newLinkTag.href = new URL(newPath, el.href).href;
  240. const removeOldEl = () => el.remove();
  241. newLinkTag.addEventListener('load', removeOldEl);
  242. newLinkTag.addEventListener('error', removeOldEl);
  243. el.after(newLinkTag);
  244. }
  245. console.log(`[vite] css hot updated: ${searchUrl}`);
  246. }
  247. });
  248. break;
  249. case 'custom': {
  250. notifyListeners(payload.event, payload.data);
  251. break;
  252. }
  253. case 'full-reload':
  254. notifyListeners('vite:beforeFullReload', payload);
  255. if (payload.path && payload.path.endsWith('.html')) {
  256. // if html file is edited, only reload the page if the browser is
  257. // currently on that page.
  258. const pagePath = decodeURI(location.pathname);
  259. const payloadPath = base + payload.path.slice(1);
  260. if (pagePath === payloadPath ||
  261. (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)) {
  262. location.reload();
  263. }
  264. return;
  265. }
  266. else {
  267. location.reload();
  268. }
  269. break;
  270. case 'prune':
  271. notifyListeners('vite:beforePrune', payload);
  272. // After an HMR update, some modules are no longer imported on the page
  273. // but they may have left behind side effects that need to be cleaned up
  274. // (.e.g style injections)
  275. // TODO Trigger their dispose callbacks.
  276. payload.paths.forEach((path) => {
  277. const fn = pruneMap.get(path);
  278. if (fn) {
  279. fn(dataMap.get(path));
  280. }
  281. });
  282. break;
  283. case 'error': {
  284. notifyListeners('vite:error', payload);
  285. const err = payload.err;
  286. if (enableOverlay) {
  287. createErrorOverlay(err);
  288. }
  289. else {
  290. console.error(`[vite] Internal Server Error\n${err.message}\n${err.stack}`);
  291. }
  292. break;
  293. }
  294. default: {
  295. const check = payload;
  296. return check;
  297. }
  298. }
  299. }
  300. function notifyListeners(event, data) {
  301. const cbs = customListenersMap.get(event);
  302. if (cbs) {
  303. cbs.forEach((cb) => cb(data));
  304. }
  305. }
  306. const enableOverlay = __HMR_ENABLE_OVERLAY__;
  307. function createErrorOverlay(err) {
  308. if (!enableOverlay)
  309. return;
  310. clearErrorOverlay();
  311. document.body.appendChild(new ErrorOverlay(err));
  312. }
  313. function clearErrorOverlay() {
  314. document
  315. .querySelectorAll(overlayId)
  316. .forEach((n) => n.close());
  317. }
  318. function hasErrorOverlay() {
  319. return document.querySelectorAll(overlayId).length;
  320. }
  321. let pending = false;
  322. let queued = [];
  323. /**
  324. * buffer multiple hot updates triggered by the same src change
  325. * so that they are invoked in the same order they were sent.
  326. * (otherwise the order may be inconsistent because of the http request round trip)
  327. */
  328. async function queueUpdate(p) {
  329. queued.push(p);
  330. if (!pending) {
  331. pending = true;
  332. await Promise.resolve();
  333. pending = false;
  334. const loading = [...queued];
  335. queued = [];
  336. (await Promise.all(loading)).forEach((fn) => fn && fn());
  337. }
  338. }
  339. async function waitForSuccessfulPing(ms = 1000) {
  340. // eslint-disable-next-line no-constant-condition
  341. while (true) {
  342. try {
  343. const pingResponse = await fetch(`${base}__vite_ping`);
  344. // success - 2xx status code
  345. if (pingResponse.ok)
  346. break;
  347. // failure - non-2xx status code
  348. else
  349. throw new Error();
  350. }
  351. catch (e) {
  352. // wait ms before attempting to ping again
  353. await new Promise((resolve) => setTimeout(resolve, ms));
  354. }
  355. }
  356. }
  357. // ping server
  358. socket.addEventListener('close', async ({ wasClean }) => {
  359. if (wasClean)
  360. return;
  361. console.log(`[vite] server connection lost. polling for restart...`);
  362. await waitForSuccessfulPing();
  363. location.reload();
  364. });
  365. const sheetsMap = new Map();
  366. function updateStyle(id, content) {
  367. let style = sheetsMap.get(id);
  368. {
  369. if (style && !(style instanceof HTMLStyleElement)) {
  370. removeStyle(id);
  371. style = undefined;
  372. }
  373. if (!style) {
  374. style = document.createElement('style');
  375. style.setAttribute('type', 'text/css');
  376. style.innerHTML = content;
  377. document.head.appendChild(style);
  378. }
  379. else {
  380. style.innerHTML = content;
  381. }
  382. }
  383. sheetsMap.set(id, style);
  384. }
  385. function removeStyle(id) {
  386. const style = sheetsMap.get(id);
  387. if (style) {
  388. if (style instanceof CSSStyleSheet) {
  389. // @ts-expect-error: using experimental API
  390. document.adoptedStyleSheets = document.adoptedStyleSheets.filter((s) => s !== style);
  391. }
  392. else {
  393. document.head.removeChild(style);
  394. }
  395. sheetsMap.delete(id);
  396. }
  397. }
  398. async function fetchUpdate({ path, acceptedPath, timestamp }) {
  399. const mod = hotModulesMap.get(path);
  400. if (!mod) {
  401. // In a code-splitting project,
  402. // it is common that the hot-updating module is not loaded yet.
  403. // https://github.com/vitejs/vite/issues/721
  404. return;
  405. }
  406. const moduleMap = new Map();
  407. const isSelfUpdate = path === acceptedPath;
  408. // make sure we only import each dep once
  409. const modulesToUpdate = new Set();
  410. if (isSelfUpdate) {
  411. // self update - only update self
  412. modulesToUpdate.add(path);
  413. }
  414. else {
  415. // dep update
  416. for (const { deps } of mod.callbacks) {
  417. deps.forEach((dep) => {
  418. if (acceptedPath === dep) {
  419. modulesToUpdate.add(dep);
  420. }
  421. });
  422. }
  423. }
  424. // determine the qualified callbacks before we re-import the modules
  425. const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
  426. return deps.some((dep) => modulesToUpdate.has(dep));
  427. });
  428. await Promise.all(Array.from(modulesToUpdate).map(async (dep) => {
  429. const disposer = disposeMap.get(dep);
  430. if (disposer)
  431. await disposer(dataMap.get(dep));
  432. const [path, query] = dep.split(`?`);
  433. try {
  434. const newMod = await import(
  435. /* @vite-ignore */
  436. base +
  437. path.slice(1) +
  438. `?import&t=${timestamp}${query ? `&${query}` : ''}`);
  439. moduleMap.set(dep, newMod);
  440. }
  441. catch (e) {
  442. warnFailedFetch(e, dep);
  443. }
  444. }));
  445. return () => {
  446. for (const { deps, fn } of qualifiedCallbacks) {
  447. fn(deps.map((dep) => moduleMap.get(dep)));
  448. }
  449. const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`;
  450. console.log(`[vite] hot updated: ${loggedPath}`);
  451. };
  452. }
  453. function sendMessageBuffer() {
  454. if (socket.readyState === 1) {
  455. messageBuffer.forEach((msg) => socket.send(msg));
  456. messageBuffer.length = 0;
  457. }
  458. }
  459. const hotModulesMap = new Map();
  460. const disposeMap = new Map();
  461. const pruneMap = new Map();
  462. const dataMap = new Map();
  463. const customListenersMap = new Map();
  464. const ctxToListenersMap = new Map();
  465. function createHotContext(ownerPath) {
  466. if (!dataMap.has(ownerPath)) {
  467. dataMap.set(ownerPath, {});
  468. }
  469. // when a file is hot updated, a new context is created
  470. // clear its stale callbacks
  471. const mod = hotModulesMap.get(ownerPath);
  472. if (mod) {
  473. mod.callbacks = [];
  474. }
  475. // clear stale custom event listeners
  476. const staleListeners = ctxToListenersMap.get(ownerPath);
  477. if (staleListeners) {
  478. for (const [event, staleFns] of staleListeners) {
  479. const listeners = customListenersMap.get(event);
  480. if (listeners) {
  481. customListenersMap.set(event, listeners.filter((l) => !staleFns.includes(l)));
  482. }
  483. }
  484. }
  485. const newListeners = new Map();
  486. ctxToListenersMap.set(ownerPath, newListeners);
  487. function acceptDeps(deps, callback = () => { }) {
  488. const mod = hotModulesMap.get(ownerPath) || {
  489. id: ownerPath,
  490. callbacks: []
  491. };
  492. mod.callbacks.push({
  493. deps,
  494. fn: callback
  495. });
  496. hotModulesMap.set(ownerPath, mod);
  497. }
  498. const hot = {
  499. get data() {
  500. return dataMap.get(ownerPath);
  501. },
  502. accept(deps, callback) {
  503. if (typeof deps === 'function' || !deps) {
  504. // self-accept: hot.accept(() => {})
  505. acceptDeps([ownerPath], ([mod]) => deps && deps(mod));
  506. }
  507. else if (typeof deps === 'string') {
  508. // explicit deps
  509. acceptDeps([deps], ([mod]) => callback && callback(mod));
  510. }
  511. else if (Array.isArray(deps)) {
  512. acceptDeps(deps, callback);
  513. }
  514. else {
  515. throw new Error(`invalid hot.accept() usage.`);
  516. }
  517. },
  518. acceptDeps() {
  519. throw new Error(`hot.acceptDeps() is deprecated. ` +
  520. `Use hot.accept() with the same signature instead.`);
  521. },
  522. dispose(cb) {
  523. disposeMap.set(ownerPath, cb);
  524. },
  525. // @ts-expect-error untyped
  526. prune(cb) {
  527. pruneMap.set(ownerPath, cb);
  528. },
  529. // TODO
  530. // eslint-disable-next-line @typescript-eslint/no-empty-function
  531. decline() { },
  532. invalidate() {
  533. // TODO should tell the server to re-perform hmr propagation
  534. // from this module as root
  535. location.reload();
  536. },
  537. // custom events
  538. on(event, cb) {
  539. const addToMap = (map) => {
  540. const existing = map.get(event) || [];
  541. existing.push(cb);
  542. map.set(event, existing);
  543. };
  544. addToMap(customListenersMap);
  545. addToMap(newListeners);
  546. },
  547. send(event, data) {
  548. messageBuffer.push(JSON.stringify({ type: 'custom', event, data }));
  549. sendMessageBuffer();
  550. }
  551. };
  552. return hot;
  553. }
  554. /**
  555. * urls here are dynamic import() urls that couldn't be statically analyzed
  556. */
  557. function injectQuery(url, queryToInject) {
  558. // skip urls that won't be handled by vite
  559. if (!url.startsWith('.') && !url.startsWith('/')) {
  560. return url;
  561. }
  562. // can't use pathname from URL since it may be relative like ../
  563. const pathname = url.replace(/#.*$/, '').replace(/\?.*$/, '');
  564. const { search, hash } = new URL(url, 'http://vitejs.dev');
  565. return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${hash || ''}`;
  566. }
  567. export { ErrorOverlay, createHotContext, injectQuery, removeStyle, updateStyle };
  568. //# sourceMappingURL=client.mjs.map