Spaces:
Runtime error
Runtime error
/** | |
* Emulates forthcoming HMR hooks in Svelte. | |
* | |
* All references to private component state ($$) are now isolated in this | |
* module. | |
*/ | |
import { | |
current_component, | |
get_current_component, | |
set_current_component, | |
} from 'svelte/internal' | |
const captureState = cmp => { | |
// sanity check: propper behaviour here is to crash noisily so that | |
// user knows that they're looking at something broken | |
if (!cmp) { | |
throw new Error('Missing component') | |
} | |
if (!cmp.$$) { | |
throw new Error('Invalid component') | |
} | |
const { | |
$$: { callbacks, bound, ctx, props }, | |
} = cmp | |
const state = cmp.$capture_state() | |
// capturing current value of props (or we'll recreate the component with the | |
// initial prop values, that may have changed -- and would not be reflected in | |
// options.props) | |
const hmr_props_values = {} | |
Object.keys(cmp.$$.props).forEach(prop => { | |
hmr_props_values[prop] = ctx[props[prop]] | |
}) | |
return { | |
ctx, | |
props, | |
callbacks, | |
bound, | |
state, | |
hmr_props_values, | |
} | |
} | |
// remapping all existing bindings (including hmr_future_foo ones) to the | |
// new version's props indexes, and refresh them with the new value from | |
// context | |
const restoreBound = (cmp, restore) => { | |
// reverse prop:ctxIndex in $$.props to ctxIndex:prop | |
// | |
// ctxIndex can be either a regular index in $$.ctx or a hmr_future_ prop | |
// | |
const propsByIndex = {} | |
for (const [name, i] of Object.entries(restore.props)) { | |
propsByIndex[i] = name | |
} | |
// NOTE $$.bound cannot change in the HMR lifetime of a component, because | |
// if bindings changes, that means the parent component has changed, | |
// which means the child (current) component will be wholly recreated | |
for (const [oldIndex, updateBinding] of Object.entries(restore.bound)) { | |
// can be either regular prop, or future_hmr_ prop | |
const propName = propsByIndex[oldIndex] | |
// this should never happen if remembering of future props is enabled... | |
// in any case, there's nothing we can do about it if we have lost prop | |
// name knowledge at this point | |
if (propName == null) continue | |
// NOTE $$.props[propName] also propagates knowledge of a possible | |
// future prop to the new $$.props (via $$.props being a Proxy) | |
const newIndex = cmp.$$.props[propName] | |
cmp.$$.bound[newIndex] = updateBinding | |
// NOTE if the prop doesn't exist or doesn't exist anymore in the new | |
// version of the component, clearing the binding is the expected | |
// behaviour (since that's what would happen in non HMR code) | |
const newValue = cmp.$$.ctx[newIndex] | |
updateBinding(newValue) | |
} | |
} | |
// restoreState | |
// | |
// It is too late to restore context at this point because component instance | |
// function has already been called (and so context has already been read). | |
// Instead, we rely on setting current_component to the same value it has when | |
// the component was first rendered -- which fix support for context, and is | |
// also generally more respectful of normal operation. | |
// | |
const restoreState = (cmp, restore) => { | |
if (!restore) return | |
if (restore.callbacks) { | |
cmp.$$.callbacks = restore.callbacks | |
} | |
if (restore.bound) { | |
restoreBound(cmp, restore) | |
} | |
// props, props.$$slots are restored at component creation (works | |
// better -- well, at all actually) | |
} | |
const get_current_component_safe = () => { | |
// NOTE relying on dynamic bindings (current_component) makes us dependent on | |
// bundler config (and apparently it does not work in demo-svelte-nollup) | |
try { | |
// unfortunately, unlike current_component, get_current_component() can | |
// crash in the normal path (when there is really no parent) | |
return get_current_component() | |
} catch (err) { | |
// ... so we need to consider that this error means that there is no parent | |
// | |
// that makes us tightly coupled to the error message but, at least, we | |
// won't mute an unexpected error, which is quite a horrible thing to do | |
if (err.message === 'Function called outside component initialization') { | |
// who knows... | |
return current_component | |
} else { | |
throw err | |
} | |
} | |
} | |
export const createProxiedComponent = ( | |
Component, | |
initialOptions, | |
{ allowLiveBinding, onInstance, onMount, onDestroy } | |
) => { | |
let cmp | |
let options = initialOptions | |
const isCurrent = _cmp => cmp === _cmp | |
const assignOptions = (target, anchor, restore, preserveLocalState) => { | |
const props = Object.assign({}, options.props) | |
// Filtering props to avoid "unexpected prop" warning | |
// NOTE this is based on props present in initial options, but it should | |
// always works, because props that are passed from the parent can't | |
// change without a code change to the parent itself -- hence, the | |
// child component will be fully recreated, and initial options should | |
// always represent props that are currnetly passed by the parent | |
if (options.props && restore.hmr_props_values) { | |
for (const prop of Object.keys(options.props)) { | |
if (restore.hmr_props_values.hasOwnProperty(prop)) { | |
props[prop] = restore.hmr_props_values[prop] | |
} | |
} | |
} | |
if (preserveLocalState && restore.state) { | |
if (Array.isArray(preserveLocalState)) { | |
// form ['a', 'b'] => preserve only 'a' and 'b' | |
props.$$inject = {} | |
for (const key of preserveLocalState) { | |
props.$$inject[key] = restore.state[key] | |
} | |
} else { | |
props.$$inject = restore.state | |
} | |
} else { | |
delete props.$$inject | |
} | |
options = Object.assign({}, initialOptions, { | |
target, | |
anchor, | |
props, | |
hydrate: false, | |
}) | |
} | |
// Preserving knowledge of "future props" -- very hackish version (maybe | |
// there should be an option to opt out of this) | |
// | |
// The use case is bind:something where something doesn't exist yet in the | |
// target component, but comes to exist later, after a HMR update. | |
// | |
// If Svelte can't map a prop in the current version of the component, it | |
// will just completely discard it: | |
// https://github.com/sveltejs/svelte/blob/1632bca34e4803d6b0e0b0abd652ab5968181860/src/runtime/internal/Component.ts#L46 | |
// | |
const rememberFutureProps = cmp => { | |
if (typeof Proxy === 'undefined') return | |
cmp.$$.props = new Proxy(cmp.$$.props, { | |
get(target, name) { | |
if (target[name] === undefined) { | |
target[name] = 'hmr_future_' + name | |
} | |
return target[name] | |
}, | |
set(target, name, value) { | |
target[name] = value | |
}, | |
}) | |
} | |
const instrument = targetCmp => { | |
const createComponent = (Component, restore, previousCmp) => { | |
set_current_component(parentComponent || previousCmp) | |
const comp = new Component(options) | |
// NOTE must be instrumented before restoreState, because restoring | |
// bindings relies on hacked $$.props | |
instrument(comp) | |
restoreState(comp, restore) | |
return comp | |
} | |
rememberFutureProps(targetCmp) | |
targetCmp.$$.on_hmr = [] | |
// `conservative: true` means we want to be sure that the new component has | |
// actually been successfuly created before destroying the old instance. | |
// This could be useful for preventing runtime errors in component init to | |
// bring down the whole HMR. Unfortunately the implementation bellow is | |
// broken (FIXME), but that remains an interesting target for when HMR hooks | |
// will actually land in Svelte itself. | |
// | |
// The goal would be to render an error inplace in case of error, to avoid | |
// losing the navigation stack (especially annoying in native, that is not | |
// based on URL navigation, so we lose the current page on each error). | |
// | |
targetCmp.$replace = ( | |
Component, | |
{ | |
target = options.target, | |
anchor = options.anchor, | |
preserveLocalState, | |
conservative = false, | |
} | |
) => { | |
const restore = captureState(targetCmp) | |
assignOptions( | |
target || options.target, | |
anchor, | |
restore, | |
preserveLocalState | |
) | |
const callbacks = cmp ? cmp.$$.on_hmr : [] | |
const afterCallbacks = callbacks.map(fn => fn(cmp)).filter(Boolean) | |
const previous = cmp | |
if (conservative) { | |
try { | |
const next = createComponent(Component, restore, previous) | |
// prevents on_destroy from firing on non-final cmp instance | |
cmp = null | |
previous.$destroy() | |
cmp = next | |
} catch (err) { | |
cmp = previous | |
throw err | |
} | |
} else { | |
// prevents on_destroy from firing on non-final cmp instance | |
cmp = null | |
if (previous) { | |
// previous can be null if last constructor has crashed | |
previous.$destroy() | |
} | |
cmp = createComponent(Component, restore, cmp) | |
} | |
cmp.$$.hmr_cmp = cmp | |
for (const fn of afterCallbacks) { | |
fn(cmp) | |
} | |
cmp.$$.on_hmr = callbacks | |
return cmp | |
} | |
// NOTE onMount must provide target & anchor (for us to be able to determinate | |
// actual DOM insertion point) | |
// | |
// And also, to support keyed list, it needs to be called each time the | |
// component is moved (same as $$.fragment.m) | |
if (onMount) { | |
const m = targetCmp.$$.fragment.m | |
targetCmp.$$.fragment.m = (...args) => { | |
const result = m(...args) | |
onMount(...args) | |
return result | |
} | |
} | |
// NOTE onDestroy must be called even if the call doesn't pass through the | |
// component's $destroy method (that we can hook onto by ourselves, since | |
// it's public API) -- this happens a lot in svelte's internals, that | |
// manipulates cmp.$$.fragment directly, often binding to fragment.d, | |
// for example | |
if (onDestroy) { | |
targetCmp.$$.on_destroy.push(() => { | |
if (isCurrent(targetCmp)) { | |
onDestroy() | |
} | |
}) | |
} | |
if (onInstance) { | |
onInstance(targetCmp) | |
} | |
// Svelte 3 creates and mount components from their constructor if | |
// options.target is present. | |
// | |
// This means that at this point, the component's `fragment.c` and, | |
// most notably, `fragment.m` will already have been called _from inside | |
// createComponent_. That is: before we have a chance to hook on it. | |
// | |
// Proxy's constructor | |
// -> createComponent | |
// -> component constructor | |
// -> component.$$.fragment.c(...) (or l, if hydrate:true) | |
// -> component.$$.fragment.m(...) | |
// | |
// -> you are here <- | |
// | |
if (onMount) { | |
const { target, anchor } = options | |
if (target) { | |
onMount(target, anchor) | |
} | |
} | |
} | |
const parentComponent = allowLiveBinding | |
? current_component | |
: get_current_component_safe() | |
cmp = new Component(options) | |
cmp.$$.hmr_cmp = cmp | |
instrument(cmp) | |
return cmp | |
} | |