Spaces:
				
			
			
	
			
			
		Runtime error
		
	
	
	
			
			
	
	
	
	
		
		
		Runtime error
		
	| /* eslint-env browser */ | |
| /** | |
| * The HMR proxy is a component-like object whose task is to sit in the | |
| * component tree in place of the proxied component, and rerender each | |
| * successive versions of said component. | |
| */ | |
| import { createProxiedComponent } from './svelte-hooks.js' | |
| const handledMethods = ['constructor', '$destroy'] | |
| const forwardedMethods = ['$set', '$on'] | |
| const logError = (msg, err) => { | |
| // eslint-disable-next-line no-console | |
| console.error('[HMR][Svelte]', msg) | |
| if (err) { | |
| // NOTE avoid too much wrapping around user errors | |
| // eslint-disable-next-line no-console | |
| 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) === '$$' | |
| // This is intented as a somewhat generic / prospective fix to the situation | |
| // that arised with the introduction of $$set in Svelte 3.24.1 -- trying to | |
| // avoid giving full knowledge (like its name) of this implementation detail | |
| // to the proxy. The $$set method can be present or not on the component, and | |
| // its presence impacts the behaviour (but with HMR it will be tested if it is | |
| // present _on the proxy_). So the idea here is to expose exactly the same $$ | |
| // props as the current version of the component and, for those that are | |
| // functions, proxy the calls to the current component. | |
| const relayInternalMethods = (proxy, cmp) => { | |
| // delete any previously added $$ prop | |
| Object.keys(proxy) | |
| .filter(isInternal) | |
| .forEach(key => { | |
| delete proxy[key] | |
| }) | |
| // guard: no component | |
| if (!cmp) return | |
| // proxy current $$ props to the actual component | |
| 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) | |
| } | |
| ) | |
| }, | |
| }) | |
| }) | |
| } | |
| // proxy custom methods | |
| 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) { | |
| // we're changing it on the real component first to see what it | |
| // gives... if it throws an error, we want to throw the same error in | |
| // order to most closely follow non-hmr behaviour. | |
| cmp[prop] = value | |
| }, | |
| }) | |
| return true | |
| } | |
| }) | |
| return wrappedProps | |
| } | |
| // everything in the constructor! | |
| // | |
| // so we don't polute the component class with new members | |
| // | |
| class ProxyComponent { | |
| constructor( | |
| { | |
| Adapter, | |
| id, | |
| debugName, | |
| current, // { Component, hotOptions: { preserveLocalState, ... } } | |
| register, | |
| }, | |
| options // { target, anchor, ... } | |
| ) { | |
| let cmp | |
| let disposed = false | |
| let lastError = null | |
| const setComponent = _cmp => { | |
| cmp = _cmp | |
| relayInternalMethods(this, cmp) | |
| } | |
| const getComponent = () => cmp | |
| const destroyComponent = () => { | |
| // destroyComponent is tolerant (don't crash on no cmp) because it | |
| // is possible that reload/rerender is called after a previous | |
| // createComponent has failed (hence we have a proxy, but no cmp) | |
| 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 || | |
| // non acceptable components (that is components that have to defer | |
| // to their parent for rerender -- e.g. accessors, named exports) | |
| // are most tricky, and they havent been considered when most of the | |
| // code has been written... as a result, they are especially tricky | |
| // to deal with, it's better to consider any error with them to be | |
| // fatal to avoid odities | |
| !current.canAccept || | |
| (err && err.hmrFatal) | |
| ) { | |
| throw err | |
| } else { | |
| // const errString = String((err && err.stack) || err) | |
| 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 | |
| // $destroy is not called when a child component is disposed, so we | |
| // need to hook from fragment. | |
| const onDestroy = () => { | |
| // NOTE do NOT call $destroy on the cmp from here; the cmp is already | |
| // dead, this would not work | |
| if (!disposed) { | |
| disposed = true | |
| adapter.dispose() | |
| unregister() | |
| } | |
| } | |
| // ---- register proxy instance ---- | |
| const unregister = register(rerender) | |
| // ---- augmented methods ---- | |
| this.$destroy = () => { | |
| destroyComponent() | |
| onDestroy() | |
| } | |
| // ---- forwarded methods ---- | |
| relayCalls(getComponent, forwardedMethods, this) | |
| // ---- create & mount target component instance --- | |
| try { | |
| let lastProperties | |
| createProxiedComponent(current.Component, options, { | |
| allowLiveBinding: current.hotOptions.allowLiveBinding, | |
| onDestroy, | |
| onMount: afterMount, | |
| onInstance: comp => { | |
| setComponent(comp) | |
| // WARNING the proxy MUST use the same $$ object as its component | |
| // instance, because a lot of wiring happens during component | |
| // initialisation... lots of references to $$ and $$.fragment have | |
| // already been distributed around when the component constructor | |
| // returns, before we have a chance to wrap them (and so we can't | |
| // wrap them no more, because existing references would become | |
| // invalid) | |
| this.$$ = comp.$$ | |
| lastProperties = copyComponentProperties(this, comp, lastProperties) | |
| }, | |
| }) | |
| } catch (err) { | |
| const { target, anchor } = options | |
| setError(err, target, anchor) | |
| throw err | |
| } | |
| } | |
| } | |
| const syncStatics = (component, proxy, previousKeys) => { | |
| // remove previously copied keys | |
| if (previousKeys) { | |
| for (const key of previousKeys) { | |
| delete proxy[key] | |
| } | |
| } | |
| // forward static properties and methods | |
| 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 | |
| /** | |
| * Creates a HMR proxy and its associated `reload` function that pushes a new | |
| * version to all existing instances of the component. | |
| */ | |
| export function createProxy({ | |
| Adapter, | |
| id, | |
| Component, | |
| hotOptions, | |
| canAccept, | |
| preserveLocalState, | |
| }) { | |
| const debugName = getDebugName(id) | |
| const instances = [] | |
| // current object will be updated, proxy instances will keep a ref | |
| const current = { | |
| Component, | |
| hotOptions, | |
| canAccept, | |
| preserveLocalState, | |
| } | |
| const name = `Proxy${debugName}` | |
| // this trick gives the dynamic name Proxy<MyComponent> to the concrete | |
| // proxy class... unfortunately, this doesn't shows in dev tools, but | |
| // it stills allow to inspect cmp.constructor.name to confirm an instance | |
| // is a proxy | |
| 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 we fail to create a proxy instance, any instance, that means | |
| // that we won't be able to fix this instance when it is updated. | |
| // Recovering to normal state will be impossible. HMR's dead. | |
| // | |
| // Fatal error will trigger a full reload on next update (reloading | |
| // right now is kinda pointless since buggy code still exists). | |
| // | |
| // NOTE Only report first error to avoid too much polution -- following | |
| // errors are probably caused by the first one, or they will show up | |
| // in turn when the first one is fixed ¯\_(ツ)_/¯ | |
| // | |
| if (!fatalError) { | |
| fatalError = true | |
| logError( | |
| `Unrecoverable HMR error in ${debugName}: ` + | |
| `next update will trigger a full reload` | |
| ) | |
| } | |
| throw err | |
| } | |
| } | |
| }, | |
| }[name] | |
| // initialize static members | |
| let previousStatics = syncStatics(current.Component, proxy) | |
| const update = newState => Object.assign(current, newState) | |
| // reload all existing instances of this component | |
| const reload = () => { | |
| fireBeforeUpdate() | |
| // copy statics before doing anything because a static prop/method | |
| // could be used somewhere in the create/render call | |
| 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 } | |
| } | |