Studying all day long)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

511 lines
15 KiB

2 years ago
import type { ErrorPayload, HMRPayload, Update } from 'types/hmrPayload'
import type { ViteHotContext } from 'types/hot'
import type { InferCustomEventPayload } from 'types/customEvent'
import { ErrorOverlay, overlayId } from './overlay'
// eslint-disable-next-line node/no-missing-import
import '@vite/env'
// injected by the hmr plugin when served
declare const __BASE__: string
declare const __HMR_PROTOCOL__: string
declare const __HMR_HOSTNAME__: string
declare const __HMR_PORT__: string
declare const __HMR_TIMEOUT__: number
declare const __HMR_ENABLE_OVERLAY__: boolean
console.log('[vite] connecting...')
// use server configuration, then fallback to inference
const socketProtocol =
__HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')
const base = __BASE__ || '/'
const messageBuffer: string[] = []
function warnFailedFetch(err: Error, path: string | string[]) {
if (!err.message.match('fetch')) {
console.error(err)
}
console.error(
`[hmr] Failed to reload ${path}. ` +
`This could be due to syntax errors or importing non-existent ` +
`modules. (see errors above)`
)
}
function cleanUrl(pathname: string): string {
const url = new URL(pathname, location.toString())
url.searchParams.delete('direct')
return url.pathname + url.search
}
// Listen for messages
socket.addEventListener('message', async ({ data }) => {
handleMessage(JSON.parse(data))
})
let isFirstUpdate = true
async function handleMessage(payload: HMRPayload) {
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
sendMessageBuffer()
// proxy(nginx, docker) hmr ws maybe caused timeout,
// so send ping package let ws keep alive.
setInterval(() => socket.send('{"type":"ping"}'), __HMR_TIMEOUT__)
break
case 'update':
notifyListeners('vite:beforeUpdate', payload)
// if this is the first update and there's already an error overlay, it
// means the page opened with existing server compile error and the whole
// module script failed to load (since one of the nested imports is 500).
// in this case a normal update won't work and a full reload is needed.
if (isFirstUpdate && hasErrorOverlay()) {
window.location.reload()
return
} else {
clearErrorOverlay()
isFirstUpdate = false
}
payload.updates.forEach((update) => {
if (update.type === 'js-update') {
queueUpdate(fetchUpdate(update))
} else {
// css-update
// this is only sent when a css file referenced with <link> is updated
const { path, timestamp } = update
const searchUrl = cleanUrl(path)
// can't use querySelector with `[href*=]` here since the link may be
// using relative paths so we need to use link.href to grab the full
// URL for the include check.
const el = Array.from(
document.querySelectorAll<HTMLLinkElement>('link')
).find((e) => cleanUrl(e.href).includes(searchUrl))
if (el) {
const newPath = `${base}${searchUrl.slice(1)}${
searchUrl.includes('?') ? '&' : '?'
}t=${timestamp}`
// rather than swapping the href on the existing tag, we will
// create a new link tag. Once the new stylesheet has loaded we
// will remove the existing link tag. This removes a Flash Of
// Unstyled Content that can occur when swapping out the tag href
// directly, as the new stylesheet has not yet been loaded.
const newLinkTag = el.cloneNode() as HTMLLinkElement
newLinkTag.href = new URL(newPath, el.href).href
const removeOldEl = () => el.remove()
newLinkTag.addEventListener('load', removeOldEl)
newLinkTag.addEventListener('error', removeOldEl)
el.after(newLinkTag)
}
console.log(`[vite] css hot updated: ${searchUrl}`)
}
})
break
case 'custom': {
notifyListeners(payload.event, payload.data)
break
}
case 'full-reload':
notifyListeners('vite:beforeFullReload', payload)
if (payload.path && payload.path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
// currently on that page.
const pagePath = decodeURI(location.pathname)
const payloadPath = base + payload.path.slice(1)
if (
pagePath === payloadPath ||
(pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)
) {
location.reload()
}
return
} else {
location.reload()
}
break
case 'prune':
notifyListeners('vite:beforePrune', payload)
// After an HMR update, some modules are no longer imported on the page
// but they may have left behind side effects that need to be cleaned up
// (.e.g style injections)
// TODO Trigger their dispose callbacks.
payload.paths.forEach((path) => {
const fn = pruneMap.get(path)
if (fn) {
fn(dataMap.get(path))
}
})
break
case 'error': {
notifyListeners('vite:error', payload)
const err = payload.err
if (enableOverlay) {
createErrorOverlay(err)
} else {
console.error(
`[vite] Internal Server Error\n${err.message}\n${err.stack}`
)
}
break
}
default: {
const check: never = payload
return check
}
}
}
function notifyListeners<T extends string>(
event: T,
data: InferCustomEventPayload<T>
): void
function notifyListeners(event: string, data: any): void {
const cbs = customListenersMap.get(event)
if (cbs) {
cbs.forEach((cb) => cb(data))
}
}
const enableOverlay = __HMR_ENABLE_OVERLAY__
function createErrorOverlay(err: ErrorPayload['err']) {
if (!enableOverlay) return
clearErrorOverlay()
document.body.appendChild(new ErrorOverlay(err))
}
function clearErrorOverlay() {
document
.querySelectorAll(overlayId)
.forEach((n) => (n as ErrorOverlay).close())
}
function hasErrorOverlay() {
return document.querySelectorAll(overlayId).length
}
let pending = false
let queued: Promise<(() => void) | undefined>[] = []
/**
* buffer multiple hot updates triggered by the same src change
* so that they are invoked in the same order they were sent.
* (otherwise the order may be inconsistent because of the http request round trip)
*/
async function queueUpdate(p: Promise<(() => void) | undefined>) {
queued.push(p)
if (!pending) {
pending = true
await Promise.resolve()
pending = false
const loading = [...queued]
queued = []
;(await Promise.all(loading)).forEach((fn) => fn && fn())
}
}
async function waitForSuccessfulPing(ms = 1000) {
// eslint-disable-next-line no-constant-condition
while (true) {
try {
const pingResponse = await fetch(`${base}__vite_ping`)
// success - 2xx status code
if (pingResponse.ok) break
// failure - non-2xx status code
else throw new Error()
} catch (e) {
// wait ms before attempting to ping again
await new Promise((resolve) => setTimeout(resolve, ms))
}
}
}
// ping server
socket.addEventListener('close', async ({ wasClean }) => {
if (wasClean) return
console.log(`[vite] server connection lost. polling for restart...`)
await waitForSuccessfulPing()
location.reload()
})
// https://wicg.github.io/construct-stylesheets
const supportsConstructedSheet = (() => {
// TODO: re-enable this try block once Chrome fixes the performance of
// rule insertion in really big stylesheets
// try {
// new CSSStyleSheet()
// return true
// } catch (e) {}
return false
})()
const sheetsMap = new Map()
export function updateStyle(id: string, content: string): void {
let style = sheetsMap.get(id)
if (supportsConstructedSheet && !content.includes('@import')) {
if (style && !(style instanceof CSSStyleSheet)) {
removeStyle(id)
style = undefined
}
if (!style) {
style = new CSSStyleSheet()
style.replaceSync(content)
// @ts-expect-error: using experimental API
document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]
} else {
style.replaceSync(content)
}
} else {
if (style && !(style instanceof HTMLStyleElement)) {
removeStyle(id)
style = undefined
}
if (!style) {
style = document.createElement('style')
style.setAttribute('type', 'text/css')
style.innerHTML = content
document.head.appendChild(style)
} else {
style.innerHTML = content
}
}
sheetsMap.set(id, style)
}
export function removeStyle(id: string): void {
const style = sheetsMap.get(id)
if (style) {
if (style instanceof CSSStyleSheet) {
// @ts-expect-error: using experimental API
document.adoptedStyleSheets = document.adoptedStyleSheets.filter(
(s: CSSStyleSheet) => s !== style
)
} else {
document.head.removeChild(style)
}
sheetsMap.delete(id)
}
}
async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
const mod = hotModulesMap.get(path)
if (!mod) {
// In a code-splitting project,
// it is common that the hot-updating module is not loaded yet.
// https://github.com/vitejs/vite/issues/721
return
}
const moduleMap = new Map()
const isSelfUpdate = path === acceptedPath
// make sure we only import each dep once
const modulesToUpdate = new Set<string>()
if (isSelfUpdate) {
// self update - only update self
modulesToUpdate.add(path)
} else {
// dep update
for (const { deps } of mod.callbacks) {
deps.forEach((dep) => {
if (acceptedPath === dep) {
modulesToUpdate.add(dep)
}
})
}
}
// determine the qualified callbacks before we re-import the modules
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
return deps.some((dep) => modulesToUpdate.has(dep))
})
await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
const disposer = disposeMap.get(dep)
if (disposer) await disposer(dataMap.get(dep))
const [path, query] = dep.split(`?`)
try {
const newMod = await import(
/* @vite-ignore */
base +
path.slice(1) +
`?import&t=${timestamp}${query ? `&${query}` : ''}`
)
moduleMap.set(dep, newMod)
} catch (e) {
warnFailedFetch(e, dep)
}
})
)
return () => {
for (const { deps, fn } of qualifiedCallbacks) {
fn(deps.map((dep) => moduleMap.get(dep)))
}
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
console.log(`[vite] hot updated: ${loggedPath}`)
}
}
function sendMessageBuffer() {
if (socket.readyState === 1) {
messageBuffer.forEach((msg) => socket.send(msg))
messageBuffer.length = 0
}
}
interface HotModule {
id: string
callbacks: HotCallback[]
}
interface HotCallback {
// the dependencies must be fetchable paths
deps: string[]
fn: (modules: object[]) => void
}
const hotModulesMap = new Map<string, HotModule>()
const disposeMap = new Map<string, (data: any) => void | Promise<void>>()
const pruneMap = new Map<string, (data: any) => void | Promise<void>>()
const dataMap = new Map<string, any>()
const customListenersMap = new Map<string, ((data: any) => void)[]>()
const ctxToListenersMap = new Map<
string,
Map<string, ((data: any) => void)[]>
>()
export function createHotContext(ownerPath: string): ViteHotContext {
if (!dataMap.has(ownerPath)) {
dataMap.set(ownerPath, {})
}
// when a file is hot updated, a new context is created
// clear its stale callbacks
const mod = hotModulesMap.get(ownerPath)
if (mod) {
mod.callbacks = []
}
// clear stale custom event listeners
const staleListeners = ctxToListenersMap.get(ownerPath)
if (staleListeners) {
for (const [event, staleFns] of staleListeners) {
const listeners = customListenersMap.get(event)
if (listeners) {
customListenersMap.set(
event,
listeners.filter((l) => !staleFns.includes(l))
)
}
}
}
const newListeners = new Map()
ctxToListenersMap.set(ownerPath, newListeners)
function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) {
const mod: HotModule = hotModulesMap.get(ownerPath) || {
id: ownerPath,
callbacks: []
}
mod.callbacks.push({
deps,
fn: callback
})
hotModulesMap.set(ownerPath, mod)
}
const hot: ViteHotContext = {
get data() {
return dataMap.get(ownerPath)
},
accept(deps?: any, callback?: any) {
if (typeof deps === 'function' || !deps) {
// self-accept: hot.accept(() => {})
acceptDeps([ownerPath], ([mod]) => deps && deps(mod))
} else if (typeof deps === 'string') {
// explicit deps
acceptDeps([deps], ([mod]) => callback && callback(mod))
} else if (Array.isArray(deps)) {
acceptDeps(deps, callback)
} else {
throw new Error(`invalid hot.accept() usage.`)
}
},
acceptDeps() {
throw new Error(
`hot.acceptDeps() is deprecated. ` +
`Use hot.accept() with the same signature instead.`
)
},
dispose(cb) {
disposeMap.set(ownerPath, cb)
},
// @ts-expect-error untyped
prune(cb: (data: any) => void) {
pruneMap.set(ownerPath, cb)
},
// TODO
// eslint-disable-next-line @typescript-eslint/no-empty-function
decline() {},
invalidate() {
// TODO should tell the server to re-perform hmr propagation
// from this module as root
location.reload()
},
// custom events
on(event, cb) {
const addToMap = (map: Map<string, any[]>) => {
const existing = map.get(event) || []
existing.push(cb)
map.set(event, existing)
}
addToMap(customListenersMap)
addToMap(newListeners)
},
send(event, data) {
messageBuffer.push(JSON.stringify({ type: 'custom', event, data }))
sendMessageBuffer()
}
}
return hot
}
/**
* urls here are dynamic import() urls that couldn't be statically analyzed
*/
export function injectQuery(url: string, queryToInject: string): string {
// skip urls that won't be handled by vite
if (!url.startsWith('.') && !url.startsWith('/')) {
return url
}
// can't use pathname from URL since it may be relative like ../
const pathname = url.replace(/#.*$/, '').replace(/\?.*$/, '')
const { search, hash } = new URL(url, 'http://vitejs.dev')
return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${
hash || ''
}`
}
export { ErrorOverlay }