348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
/**
|
|
* 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
|
|
}
|