Skip to content

Commit

Permalink
sveltejs#43 fixes preservation of bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
rixo committed Mar 3, 2022
1 parent 90b3fbc commit 009eb92
Show file tree
Hide file tree
Showing 2 changed files with 310 additions and 12 deletions.
245 changes: 243 additions & 2 deletions packages/svelte-hmr-spec/test/bindings.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,7 @@ describe('bindings', () => {
`

// TODO should depend on preserveLocalState option
testHmr.skip`
testHmr`
# resets bound values when owner is updated
--- App.svelte ---
Expand Down Expand Up @@ -86,4 +85,246 @@ describe('bindings', () => {
<input type="number" />
<div>123</div>
`

testHmr`
# instance function are preserved when binding to instance
--- App.svelte ---
<script>
import {onMount} from 'svelte'
import Foo from './Foo.svelte'
let foo
let x
const update = () => {
x = foo.get()
}
onMount(update)
</script>
<Foo bind:this={foo} />
<x-focus>{x}</x-focus>
<button on:click={update} />
--- Foo.svelte ---
<script>
::0 export const get = () => 1
::1 export const get = () => 2
</script>
* * * * *
::0::
1
::1::
${clickButton()}
2
`

testHmr`
# const function bindings are preserved
--- App.svelte ---
<script>
import {onMount} from 'svelte'
import Foo from './Foo.svelte'
let get
let x
const update = () => {
x = get()
}
onMount(update)
</script>
<Foo bind:get />
<x-focus>{x}</x-focus>
<button on:click={update} />
--- Foo.svelte ---
<script>
::0 export const get = () => 1
::1 export const get = () => 2
</script>
* * * * *
::0::
1
::1::
${clickButton()}
2
`

testHmr`
# const function bindings are preserved when variables change
--- App.svelte ---
<script>
import {onMount} from 'svelte'
import Foo from './Foo.svelte'
let get
let x
const update = () => {
x = get && get()
}
onMount(update)
</script>
<Foo bind:get />
<x-focus>{x}</x-focus>
<button on:click={update} />
--- Foo.svelte ---
<script>
::0 export const get = () => 1
::1 let foo = 'FOO'
::1 export let bar = 'BAR'
::1 export const get = () => 2
::1 $: console.log(foo + bar)
::2 let foo = 'FOO'
::2 $: console.log(foo)
::2 export const get = () => 3
::3 export const set = () => {}
::4 let foo = 'FOO'; let bar = 'BAR'; let baz = 'BAZ'; let bat = 'BAT';
::4 $: console.log(foo + bar + baz + bat)
::4 export const get = () => 4
</script>
* * * * *
::0::
1
::1:: exported function order in variables changes
1
${clickButton()}
2
::2:: exported function order in variables changes again
2
${clickButton()}
3
::3:: exported function disappears
3
${clickButton()}
undefined
::4:: exported function comes back (at another index)
undefined
${clickButton()}
4
`

testHmr`
# let function bindings are preserved
--- App.svelte ---
<script>
import {onMount} from 'svelte'
import Foo from './Foo.svelte'
let get
let x
const update = () => {
x = get()
}
onMount(update)
</script>
<Foo bind:get />
<x-focus>{x}</x-focus>
<button id="update" on:click={update} />
--- Foo.svelte ---
<script>
::0 export let get = () => 1
::1 export let get = () => 2
export const change = () => {
get = () => 3
}
</script>
<button id="change-let" on:click={change} />
* * * * *
::0::
1
::1::
${clickButton('#update')}
2
${clickButton('#change-let')}
2
${clickButton('#update')}
3
`

testHmr`
# binding to a prop that does not exists yet
--- App.svelte ---
<script>
import {onMount} from 'svelte'
import Foo from './Foo.svelte'
let get
let x
const update = () => {
x = get && get()
}
onMount(update)
</script>
<Foo bind:get />
<x-focus>{x}</x-focus>
<button on:click={update} />
--- Foo.svelte ---
<script>
::0 export let bet = () => 1
::1 export let get = () => 2
</script>
* * * * *
::0::
undefined
::1::
${clickButton()}
2
`
})
77 changes: 67 additions & 10 deletions packages/svelte-hmr/runtime/svelte-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,28 @@ const captureState = cmp => {
}

const {
$$: { callbacks, bound, ctx },
$$: { callbacks, bound, ctx, props, hmr_future_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 props = Object.assign({}, cmp.$$.props)
const hmr_props_value = {}
Object.keys(cmp.$$.props).forEach(prop => {
props[prop] = ctx[props[prop]]
hmr_props_value[prop] = ctx[props[prop]]
})

return { ctx, callbacks, bound, state, props }
return {
ctx,
props,
hmr_future_props,
callbacks,
bound,
state,
hmr_props_value,
}
}

// restoreState
Expand All @@ -49,12 +57,28 @@ const restoreState = (cmp, restore) => {
if (!restore) {
return
}
const { callbacks, bound } = restore
const { callbacks, bound, hmr_future_props } = restore

if (callbacks) {
cmp.$$.callbacks = callbacks
}

if (bound) {
cmp.$$.bound = bound
const propsByIndex = {}
for (const [name, i] of Object.entries(restore.props)) {
propsByIndex[i] = name
}
for (const oldIndex of Object.keys(bound)) {
const callback = bound[oldIndex]
const propName = hmr_future_props[-oldIndex - 1] || propsByIndex[oldIndex]
if (propName == null) continue
const newIndex = cmp.$$.props[propName]
cmp.$$.bound[newIndex] = callback
// 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)
callback(cmp.$$.ctx[newIndex])
}
}
// props, props.$$slots are restored at component creation (works
// better -- well, at all actually)
Expand Down Expand Up @@ -100,10 +124,10 @@ export const createProxiedComponent = (
// 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.props) {
if (options.props && restore.hmr_props_value) {
for (const prop of Object.keys(options.props)) {
if (restore.props.hasOwnProperty(prop)) {
props[prop] = restore.props[prop]
if (restore.hmr_props_value.hasOwnProperty(prop)) {
props[prop] = restore.hmr_props_value[prop]
}
}
}
Expand All @@ -129,15 +153,48 @@ export const createProxiedComponent = (
})
}

// 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 => {
cmp.$$.hmr_future_props = []

if (typeof Proxy === 'undefined') return

cmp.$$.props = new Proxy(cmp.$$.props, {
get(target, name) {
if (target[name] === undefined) {
cmp.$$.hmr_future_props.push(name)
return -cmp.$$.hmr_future_props.length
}
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)
restoreState(comp, restore)
// 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
Expand Down

0 comments on commit 009eb92

Please sign in to comment.