diff --git a/.changeset/khaki-pets-exercise.md b/.changeset/khaki-pets-exercise.md
new file mode 100644
index 0000000..6a79428
--- /dev/null
+++ b/.changeset/khaki-pets-exercise.md
@@ -0,0 +1,5 @@
+---
+'svelte-hmr': patch
+---
+
+Fix preserving bind: directive (fixes #43)
diff --git a/packages/svelte-hmr-spec/test/bindings.spec.js b/packages/svelte-hmr-spec/test/bindings.spec.js
index f17dd18..d6723f6 100644
--- a/packages/svelte-hmr-spec/test/bindings.spec.js
+++ b/packages/svelte-hmr-spec/test/bindings.spec.js
@@ -56,8 +56,7 @@ describe('bindings', () => {
`
- // TODO should depend on preserveLocalState option
- testHmr.skip`
+ testHmr`
# resets bound values when owner is updated
--- App.svelte ---
@@ -86,4 +85,257 @@ describe('bindings', () => {
123
`
+
+ testHmr`
+ # instance function are preserved when binding to instance
+
+ --- App.svelte ---
+
+
+
+
+
+ {x}
+
+
+
+ --- Foo.svelte ---
+
+
+
+ * * * * *
+
+ ::0::
+ 1
+ ::1::
+ ${clickButton()}
+ 2
+ `
+
+ testHmr`
+ # const function bindings are preserved
+
+ --- App.svelte ---
+
+
+
+
+
+ {x}
+
+
+
+ --- Foo.svelte ---
+
+
+
+ * * * * *
+
+ ::0::
+ 1
+ ::1::
+ ${clickButton()}
+ 2
+ `
+
+ testHmr`
+ # const function bindings are preserved when variables change
+
+ --- App.svelte ---
+
+
+
+
+
+ {x}
+
+
+
+ --- Foo.svelte ---
+
+
+
+ * * * * *
+
+ ::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 ---
+
+
+
+
+
+ {x}
+
+
+
+ --- Foo.svelte ---
+
+
+
+
+
+ * * * * *
+
+ ::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 ---
+
+
+
+
+
+ {x}
+
+
+
+ --- Foo.svelte ---
+
+
+
+ * * * * *
+
+ ::0::
+ undefined
+ ::1::
+ undefined
+ ${clickButton()}
+ 2
+ ::2:: doesn't reuse a wrong variable in the right place
+ 2
+ ${clickButton()}
+ undefined
+ ::3:: remembers older future prop
+ undefined
+ ${clickButton()}
+ 4
+ `
})
diff --git a/packages/svelte-hmr/runtime/svelte-hooks.js b/packages/svelte-hmr/runtime/svelte-hooks.js
index 1e95dc5..864aafb 100644
--- a/packages/svelte-hmr/runtime/svelte-hooks.js
+++ b/packages/svelte-hmr/runtime/svelte-hooks.js
@@ -21,7 +21,7 @@ const captureState = cmp => {
}
const {
- $$: { callbacks, bound, ctx },
+ $$: { callbacks, bound, ctx, props },
} = cmp
const state = cmp.$capture_state()
@@ -29,12 +29,57 @@ const captureState = cmp => {
// 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_values = {}
Object.keys(cmp.$$.props).forEach(prop => {
- props[prop] = ctx[props[prop]]
+ hmr_props_values[prop] = ctx[props[prop]]
})
- return { ctx, callbacks, bound, state, props }
+ 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
@@ -46,16 +91,16 @@ const captureState = cmp => {
// also generally more respectful of normal operation.
//
const restoreState = (cmp, restore) => {
- if (!restore) {
- return
- }
- const { callbacks, bound } = restore
- if (callbacks) {
- cmp.$$.callbacks = callbacks
+ if (!restore) return
+
+ if (restore.callbacks) {
+ cmp.$$.callbacks = restore.callbacks
}
- if (bound) {
- cmp.$$.bound = bound
+
+ if (restore.bound) {
+ restoreBound(cmp, restore)
}
+
// props, props.$$slots are restored at component creation (works
// better -- well, at all actually)
}
@@ -100,10 +145,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_values) {
for (const prop of Object.keys(options.props)) {
- if (restore.props.hasOwnProperty(prop)) {
- props[prop] = restore.props[prop]
+ if (restore.hmr_props_values.hasOwnProperty(prop)) {
+ props[prop] = restore.hmr_props_values[prop]
}
}
}
@@ -129,15 +174,45 @@ 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 => {
+ 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)
- 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
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cef3994..0e99ae1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -95,7 +95,7 @@ importers:
rollup-plugin-svelte-hot: github:rixo/rollup-plugin-svelte-hot#svhs
rollup-plugin-terser: ^5.1.2
sirv-cli: ^0.4.4
- svelte: ~3.41.0
+ svelte: ~3.46.4
svelte-hmr: workspace:*
svelte-hmr-spec: workspace:*
dependencies:
@@ -108,9 +108,9 @@ importers:
rollup: 2.54.0
rollup-plugin-hot: 0.1.1_rollup@2.54.0
rollup-plugin-livereload: 1.3.0
- rollup-plugin-svelte-hot: github.com/rixo/rollup-plugin-svelte-hot/5911fabc970c634f78c901e023526c81990e7474_bce53102debd41c5dac09b4d251cb2d9
+ rollup-plugin-svelte-hot: github.com/rixo/rollup-plugin-svelte-hot/5911fabc970c634f78c901e023526c81990e7474_0976037ac3c77f7249b7fcdddb456ee6
rollup-plugin-terser: 5.3.1_rollup@2.54.0
- svelte: 3.41.0
+ svelte: 3.46.4
svelte-hmr: link:../../../packages/svelte-hmr
svelte-hmr-spec: link:../../../packages/svelte-hmr-spec
@@ -3305,7 +3305,7 @@ packages:
/jsonfile/4.0.0:
resolution: {integrity: sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=}
optionalDependencies:
- graceful-fs: 4.2.6
+ graceful-fs: 4.2.9
dev: true
/just-extend/4.2.1:
@@ -3388,7 +3388,7 @@ packages:
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
engines: {node: '>=6'}
dependencies:
- graceful-fs: 4.2.6
+ graceful-fs: 4.2.9
js-yaml: 3.14.1
pify: 4.0.1
strip-bom: 3.0.0
@@ -5281,13 +5281,13 @@ packages:
engines: {node: '>= 8'}
dev: true
- /svelte/3.41.0:
- resolution: {integrity: sha512-X9/lnTcRBCrMdyFBVjfmqy1T2vyN8ejUE1OfbWSccc2Z42Amn3ab3XdBgVl+oDkZvzPfPMoxo6CEbWca7pXOew==}
+ /svelte/3.44.1:
+ resolution: {integrity: sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==}
engines: {node: '>= 8'}
dev: true
- /svelte/3.44.1:
- resolution: {integrity: sha512-4DrCEJoBvdR689efHNSxIQn2pnFwB7E7j2yLEJtHE/P8hxwZWIphCtJ8are7bjl/iVMlcEf5uh5pJ68IwR09vQ==}
+ /svelte/3.46.4:
+ resolution: {integrity: sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==}
engines: {node: '>= 8'}
dev: true
@@ -5908,7 +5908,7 @@ packages:
zora: 3.1.9
dev: true
- github.com/rixo/rollup-plugin-svelte-hot/5911fabc970c634f78c901e023526c81990e7474_bce53102debd41c5dac09b4d251cb2d9:
+ github.com/rixo/rollup-plugin-svelte-hot/5911fabc970c634f78c901e023526c81990e7474_0976037ac3c77f7249b7fcdddb456ee6:
resolution: {tarball: https://codeload.github.com/rixo/rollup-plugin-svelte-hot/tar.gz/5911fabc970c634f78c901e023526c81990e7474}
id: github.com/rixo/rollup-plugin-svelte-hot/5911fabc970c634f78c901e023526c81990e7474
name: rollup-plugin-svelte-hot
@@ -5928,7 +5928,7 @@ packages:
rollup-plugin-hot-nollup: 0.1.2_rollup@2.54.0
rollup-pluginutils: 2.8.2
sourcemap-codec: 1.4.8
- svelte: 3.41.0
+ svelte: 3.46.4
dev: true
github.com/rixo/zora-reporters/002538be1e3fc28bc4bdca8e1bed15b20c8bd2e6:
diff --git a/test/apps/svhs/package.json b/test/apps/svhs/package.json
index c47e2e5..16ba92d 100644
--- a/test/apps/svhs/package.json
+++ b/test/apps/svhs/package.json
@@ -15,7 +15,7 @@
"rollup-plugin-livereload": "^1.0.0",
"rollup-plugin-svelte-hot": "github:rixo/rollup-plugin-svelte-hot#svhs",
"rollup-plugin-terser": "^5.1.2",
- "svelte": "~3.41.0",
+ "svelte": "~3.46.4",
"svelte-hmr": "workspace:*",
"svelte-hmr-spec": "workspace:*"
},