|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { createProxiedComponent } from './svelte-hooks.js' |
|
|
|
const handledMethods = ['constructor', '$destroy'] |
|
const forwardedMethods = ['$set', '$on'] |
|
|
|
const logError = (msg, err) => { |
|
|
|
console.error('[HMR][Svelte]', msg) |
|
if (err) { |
|
|
|
|
|
console.error(err) |
|
} |
|
} |
|
|
|
const posixify = file => file.replace(/[/\\]/g, '/') |
|
|
|
const getBaseName = id => |
|
id |
|
.split('/') |
|
.pop() |
|
.split('.') |
|
.slice(0, -1) |
|
.join('.') |
|
|
|
const capitalize = str => str[0].toUpperCase() + str.slice(1) |
|
|
|
const getFriendlyName = id => capitalize(getBaseName(posixify(id))) |
|
|
|
const getDebugName = id => `<${getFriendlyName(id)}>` |
|
|
|
const relayCalls = (getTarget, names, dest = {}) => { |
|
for (const key of names) { |
|
dest[key] = function(...args) { |
|
const target = getTarget() |
|
if (!target) { |
|
return |
|
} |
|
return target[key] && target[key].call(this, ...args) |
|
} |
|
} |
|
return dest |
|
} |
|
|
|
const isInternal = key => key !== '$$' && key.slice(0, 2) === '$$' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const relayInternalMethods = (proxy, cmp) => { |
|
|
|
Object.keys(proxy) |
|
.filter(isInternal) |
|
.forEach(key => { |
|
delete proxy[key] |
|
}) |
|
|
|
if (!cmp) return |
|
|
|
Object.keys(cmp) |
|
.filter(isInternal) |
|
.forEach(key => { |
|
Object.defineProperty(proxy, key, { |
|
configurable: true, |
|
get() { |
|
const value = cmp[key] |
|
if (typeof value !== 'function') return value |
|
return ( |
|
value && |
|
function(...args) { |
|
return value.apply(this, args) |
|
} |
|
) |
|
}, |
|
}) |
|
}) |
|
} |
|
|
|
|
|
const copyComponentProperties = (proxy, cmp, previous) => { |
|
if (previous) { |
|
previous.forEach(prop => { |
|
delete proxy[prop] |
|
}) |
|
} |
|
|
|
const props = Object.getOwnPropertyNames(Object.getPrototypeOf(cmp)) |
|
const wrappedProps = props.filter(prop => { |
|
if (!handledMethods.includes(prop) && !forwardedMethods.includes(prop)) { |
|
Object.defineProperty(proxy, prop, { |
|
configurable: true, |
|
get() { |
|
return cmp[prop] |
|
}, |
|
set(value) { |
|
|
|
|
|
|
|
cmp[prop] = value |
|
}, |
|
}) |
|
return true |
|
} |
|
}) |
|
|
|
return wrappedProps |
|
} |
|
|
|
|
|
|
|
|
|
|
|
class ProxyComponent { |
|
constructor( |
|
{ |
|
Adapter, |
|
id, |
|
debugName, |
|
current, // { Component, hotOptions: { preserveLocalState, ... } } |
|
register, |
|
}, |
|
options |
|
) { |
|
let cmp |
|
let disposed = false |
|
let lastError = null |
|
|
|
const setComponent = _cmp => { |
|
cmp = _cmp |
|
relayInternalMethods(this, cmp) |
|
} |
|
|
|
const getComponent = () => cmp |
|
|
|
const destroyComponent = () => { |
|
|
|
|
|
|
|
if (cmp) { |
|
cmp.$destroy() |
|
setComponent(null) |
|
} |
|
} |
|
|
|
const refreshComponent = (target, anchor, conservativeDestroy) => { |
|
if (lastError) { |
|
lastError = null |
|
adapter.rerender() |
|
} else { |
|
try { |
|
const replaceOptions = { |
|
target, |
|
anchor, |
|
preserveLocalState: current.preserveLocalState, |
|
} |
|
if (conservativeDestroy) { |
|
replaceOptions.conservativeDestroy = true |
|
} |
|
cmp.$replace(current.Component, replaceOptions) |
|
} catch (err) { |
|
setError(err, target, anchor) |
|
if ( |
|
!current.hotOptions.optimistic || |
|
|
|
|
|
|
|
|
|
|
|
|
|
!current.canAccept || |
|
(err && err.hmrFatal) |
|
) { |
|
throw err |
|
} else { |
|
|
|
logError(`Error during component init: ${debugName}`, err) |
|
} |
|
} |
|
} |
|
} |
|
|
|
const setError = err => { |
|
lastError = err |
|
adapter.renderError(err) |
|
} |
|
|
|
const instance = { |
|
hotOptions: current.hotOptions, |
|
proxy: this, |
|
id, |
|
debugName, |
|
refreshComponent, |
|
} |
|
|
|
const adapter = new Adapter(instance) |
|
|
|
const { afterMount, rerender } = adapter |
|
|
|
|
|
|
|
const onDestroy = () => { |
|
|
|
|
|
if (!disposed) { |
|
disposed = true |
|
adapter.dispose() |
|
unregister() |
|
} |
|
} |
|
|
|
|
|
|
|
const unregister = register(rerender) |
|
|
|
|
|
|
|
this.$destroy = () => { |
|
destroyComponent() |
|
onDestroy() |
|
} |
|
|
|
|
|
|
|
relayCalls(getComponent, forwardedMethods, this) |
|
|
|
|
|
|
|
try { |
|
let lastProperties |
|
createProxiedComponent(current.Component, options, { |
|
allowLiveBinding: current.hotOptions.allowLiveBinding, |
|
onDestroy, |
|
onMount: afterMount, |
|
onInstance: comp => { |
|
setComponent(comp) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.$$ = comp.$$ |
|
lastProperties = copyComponentProperties(this, comp, lastProperties) |
|
}, |
|
}) |
|
} catch (err) { |
|
const { target, anchor } = options |
|
setError(err, target, anchor) |
|
throw err |
|
} |
|
} |
|
} |
|
|
|
const syncStatics = (component, proxy, previousKeys) => { |
|
|
|
if (previousKeys) { |
|
for (const key of previousKeys) { |
|
delete proxy[key] |
|
} |
|
} |
|
|
|
|
|
const keys = [] |
|
for (const key in component) { |
|
keys.push(key) |
|
proxy[key] = component[key] |
|
} |
|
|
|
return keys |
|
} |
|
|
|
const globalListeners = {} |
|
|
|
const onGlobal = (event, fn) => { |
|
event = event.toLowerCase() |
|
if (!globalListeners[event]) globalListeners[event] = [] |
|
globalListeners[event].push(fn) |
|
} |
|
|
|
const fireGlobal = (event, ...args) => { |
|
const listeners = globalListeners[event] |
|
if (!listeners) return |
|
for (const fn of listeners) { |
|
fn(...args) |
|
} |
|
} |
|
|
|
const fireBeforeUpdate = () => fireGlobal('beforeupdate') |
|
|
|
const fireAfterUpdate = () => fireGlobal('afterupdate') |
|
|
|
if (typeof window !== 'undefined') { |
|
window.__SVELTE_HMR = { |
|
on: onGlobal, |
|
} |
|
window.dispatchEvent(new CustomEvent('svelte-hmr:ready')) |
|
} |
|
|
|
let fatalError = false |
|
|
|
export const hasFatalError = () => fatalError |
|
|
|
|
|
|
|
|
|
|
|
export function createProxy({ |
|
Adapter, |
|
id, |
|
Component, |
|
hotOptions, |
|
canAccept, |
|
preserveLocalState, |
|
}) { |
|
const debugName = getDebugName(id) |
|
const instances = [] |
|
|
|
|
|
const current = { |
|
Component, |
|
hotOptions, |
|
canAccept, |
|
preserveLocalState, |
|
} |
|
|
|
const name = `Proxy${debugName}` |
|
|
|
|
|
|
|
|
|
|
|
const proxy = { |
|
[name]: class extends ProxyComponent { |
|
constructor(options) { |
|
try { |
|
super( |
|
{ |
|
Adapter, |
|
id, |
|
debugName, |
|
current, |
|
register: rerender => { |
|
instances.push(rerender) |
|
const unregister = () => { |
|
const i = instances.indexOf(rerender) |
|
instances.splice(i, 1) |
|
} |
|
return unregister |
|
}, |
|
}, |
|
options |
|
) |
|
} catch (err) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!fatalError) { |
|
fatalError = true |
|
logError( |
|
`Unrecoverable HMR error in ${debugName}: ` + |
|
`next update will trigger a full reload` |
|
) |
|
} |
|
throw err |
|
} |
|
} |
|
}, |
|
}[name] |
|
|
|
|
|
let previousStatics = syncStatics(current.Component, proxy) |
|
|
|
const update = newState => Object.assign(current, newState) |
|
|
|
|
|
const reload = () => { |
|
fireBeforeUpdate() |
|
|
|
|
|
|
|
previousStatics = syncStatics(current.Component, proxy, previousStatics) |
|
|
|
const errors = [] |
|
|
|
instances.forEach(rerender => { |
|
try { |
|
rerender() |
|
} catch (err) { |
|
logError(`Failed to rerender ${debugName}`, err) |
|
errors.push(err) |
|
} |
|
}) |
|
|
|
if (errors.length > 0) { |
|
return false |
|
} |
|
|
|
fireAfterUpdate() |
|
|
|
return true |
|
} |
|
|
|
const hasFatalError = () => fatalError |
|
|
|
return { id, proxy, update, reload, hasFatalError, current } |
|
} |
|
|