UNPKG

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